P5a plan: PDF documents — seven Ornith tasks with acceptance tests first
docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables, 04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI, skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted fixtures. Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard; TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and writeTimeout 120→240 s for the PDF path's worst case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+223
@@ -0,0 +1,223 @@
|
||||
package pdffont
|
||||
|
||||
import "github.com/go-pdfkit/reader"
|
||||
|
||||
// readSimple reads a font addressed one byte at a time: what each code is
|
||||
// called, and how wide it is.
|
||||
func (f *Font) readSimple() {
|
||||
f.descriptor, _ = f.doc.GetDict(f.dict, "FontDescriptor")
|
||||
f.symbolic = symbolicFlag(f.doc, f.descriptor)
|
||||
f.names = f.readEncoding()
|
||||
f.readSimpleWidths()
|
||||
}
|
||||
|
||||
// readType3 reads a font whose glyphs are little drawings.
|
||||
func (f *Font) readType3() {
|
||||
f.descriptor, _ = f.doc.GetDict(f.dict, "FontDescriptor")
|
||||
f.names = f.readEncoding()
|
||||
f.charProcs, _ = f.doc.GetDict(f.dict, "CharProcs")
|
||||
f.t3Res, _ = f.doc.GetDict(f.dict, "Resources")
|
||||
if m := floatArray(f.doc, f.dict.Get("FontMatrix")); len(m) >= 6 {
|
||||
copy(f.fontMatrix[:], m[:6])
|
||||
}
|
||||
f.readSimpleWidths()
|
||||
// A Type 3 font's widths are in its own glyph space, which the font
|
||||
// matrix maps onto text space.
|
||||
for code, w := range f.widths {
|
||||
f.widths[code] = w * 1000 * f.fontMatrix[0]
|
||||
}
|
||||
f.defaultW = 0
|
||||
}
|
||||
|
||||
// symbolicFlag reads whether a font says it is addressed through its own
|
||||
// character map rather than through an encoding of names.
|
||||
func symbolicFlag(d *reader.Document, descriptor reader.Dict) bool {
|
||||
flags, ok := reader.ToInt(resolve(d, descriptor.Get("Flags")))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
const symbolic, nonSymbolic = 1 << 2, 1 << 5
|
||||
return flags&symbolic != 0 && flags&nonSymbolic == 0
|
||||
}
|
||||
|
||||
// readEncoding works out what each code of a simple font is called: a base
|
||||
// table, and then whatever differences the font names on top of it.
|
||||
func (f *Font) readEncoding() map[int]string {
|
||||
base := StandardEncoding
|
||||
out := map[int]string{}
|
||||
f.chosen = map[int]bool{}
|
||||
enc := resolve(f.doc, f.dict.Get("Encoding"))
|
||||
if name, ok := reader.ToName(enc); ok {
|
||||
table := NamedEncoding(name, base)
|
||||
fillNames(out, table)
|
||||
// A named encoding is the document's own word on what its codes are,
|
||||
// symbolic font or not.
|
||||
for code, n := range table {
|
||||
if n != "" {
|
||||
f.chosen[code] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
encDict, ok := reader.ToDict(enc)
|
||||
if !ok {
|
||||
fillNames(out, base)
|
||||
return out
|
||||
}
|
||||
if name, ok := reader.ToName(resolve(f.doc, encDict.Get("BaseEncoding"))); ok {
|
||||
base = NamedEncoding(name, base)
|
||||
}
|
||||
fillNames(out, base)
|
||||
f.applyDifferences(out, encDict)
|
||||
return out
|
||||
}
|
||||
|
||||
// NamedEncoding is the table a name stands for, or the fallback for a name
|
||||
// that is not one of them.
|
||||
func NamedEncoding(name reader.Name, fallback [256]string) [256]string {
|
||||
switch name {
|
||||
case "WinAnsiEncoding":
|
||||
return WinAnsiEncoding
|
||||
case "StandardEncoding", "MacRomanEncoding", "MacExpertEncoding":
|
||||
// Mac Roman differs from the standard encoding above code 127, where
|
||||
// almost nothing in a Latin document lives; using the standard table
|
||||
// is closer than using none.
|
||||
return StandardEncoding
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// fillNames copies a table into the map the rest of this works from.
|
||||
func fillNames(out map[int]string, table [256]string) {
|
||||
for code, name := range table {
|
||||
if name != "" {
|
||||
out[code] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// applyDifferences reads the array that names glyphs a code at a time: a
|
||||
// number, then the names of the codes from there on.
|
||||
func (f *Font) applyDifferences(out map[int]string, encDict reader.Dict) {
|
||||
arr, ok := reader.ToArray(resolve(f.doc, encDict.Get("Differences")))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
code := 0
|
||||
for _, e := range arr {
|
||||
v := resolve(f.doc, e)
|
||||
if n, ok := reader.ToInt(v); ok {
|
||||
code = int(n)
|
||||
continue
|
||||
}
|
||||
if name, ok := reader.ToName(v); ok {
|
||||
if code >= 0 && code < 256 {
|
||||
out[code] = string(name)
|
||||
f.chosen[code] = true
|
||||
}
|
||||
code++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readSimpleWidths reads /Widths, which gives one width per code from
|
||||
// /FirstChar on.
|
||||
func (f *Font) readSimpleWidths() {
|
||||
first, ok := reader.ToInt(resolve(f.doc, f.dict.Get("FirstChar")))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
arr, ok := reader.ToArray(resolve(f.doc, f.dict.Get("Widths")))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i, e := range arr {
|
||||
if v, ok := reader.ToFloat(resolve(f.doc, e)); ok {
|
||||
f.widths[int(first)+i] = v / 1000
|
||||
}
|
||||
}
|
||||
if mw := floatOf(resolve(f.doc, f.descriptor.Get("MissingWidth")), -1); mw >= 0 {
|
||||
f.defaultW = mw / 1000
|
||||
}
|
||||
}
|
||||
|
||||
// readComposite reads a font addressed by character identifier: its descendant
|
||||
// holds the widths, the descriptor and the map from identifier to glyph.
|
||||
func (f *Font) readComposite() {
|
||||
f.defaultW = 1
|
||||
arr, ok := reader.ToArray(resolve(f.doc, f.dict.Get("DescendantFonts")))
|
||||
if !ok || len(arr) == 0 {
|
||||
return
|
||||
}
|
||||
kid, ok := reader.ToDict(resolve(f.doc, arr[0]))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v, ok := reader.ToFloat(resolve(f.doc, kid.Get("DW"))); ok {
|
||||
f.defaultW = v / 1000
|
||||
}
|
||||
f.readCIDWidths(kid)
|
||||
f.descriptor, _ = f.doc.GetDict(kid, "FontDescriptor")
|
||||
f.symbolic = symbolicFlag(f.doc, f.descriptor)
|
||||
if stream, ok := reader.ToStream(resolve(f.doc, kid.Get("CIDToGIDMap"))); ok {
|
||||
if data, img, err := f.doc.DecodeStream(stream); err == nil && img == "" {
|
||||
f.cidToGID = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readCIDWidths reads the /W array, which names widths one identifier at a
|
||||
// time or a run at a time.
|
||||
func (f *Font) readCIDWidths(kid reader.Dict) {
|
||||
arr, ok := reader.ToArray(resolve(f.doc, kid.Get("W")))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(arr); {
|
||||
first, ok := reader.ToInt(resolve(f.doc, arr[i]))
|
||||
if !ok || i+1 >= len(arr) {
|
||||
return
|
||||
}
|
||||
next := resolve(f.doc, arr[i+1])
|
||||
if list, ok := reader.ToArray(next); ok {
|
||||
for k, e := range list {
|
||||
if v, ok := reader.ToFloat(resolve(f.doc, e)); ok {
|
||||
f.widths[int(first)+k] = v / 1000
|
||||
}
|
||||
}
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
last, ok := reader.ToInt(next)
|
||||
if !ok || i+2 >= len(arr) {
|
||||
return
|
||||
}
|
||||
w, ok := reader.ToFloat(resolve(f.doc, arr[i+2]))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if last >= first && last-first < 1<<20 {
|
||||
for c := first; c <= last; c++ {
|
||||
f.widths[int(c)] = w / 1000
|
||||
}
|
||||
}
|
||||
i += 3
|
||||
}
|
||||
}
|
||||
|
||||
// floatArray reads an array of numbers, or nothing.
|
||||
func floatArray(d *reader.Document, o reader.Object) []float64 {
|
||||
arr, ok := reader.ToArray(resolve(d, o))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]float64, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
v, ok := reader.ToFloat(resolve(d, e))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user