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:
+175
@@ -0,0 +1,175 @@
|
||||
package extract
|
||||
|
||||
import (
|
||||
"github.com/go-opentype/opentype"
|
||||
"github.com/go-pdfkit/pdffont"
|
||||
"github.com/go-pdfkit/reader"
|
||||
)
|
||||
|
||||
// attachProgram gives a font a last resort for the codes the document says
|
||||
// nothing about: what its own embedded program calls them.
|
||||
//
|
||||
// A font that says it is symbolic is addressed through its own character map,
|
||||
// and an assumed encoding is a bad guess at it — a mathematical font puts a
|
||||
// capital gamma where the standard encoding puts an inverted exclamation mark.
|
||||
// The program knows, and this is how it is asked.
|
||||
//
|
||||
// There are two ways to ask, and a program answers at most one of them. A
|
||||
// PostScript program names its glyphs and carries an encoding from code to
|
||||
// name, so the name is the answer. A TrueType program usually does neither —
|
||||
// a subset of one names nothing worth reading and has no code-to-name
|
||||
// encoding at all — but it does carry character maps, and walking through one
|
||||
// to the glyph and back out of another says which character the glyph is for.
|
||||
func attachProgram(f *pdffont.Font) {
|
||||
if f.Kind() == pdffont.Composite {
|
||||
return
|
||||
}
|
||||
key, data, ok := f.Program()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
program, err := readProgram(key, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
maps := chooseCharacterMaps(program)
|
||||
// Only a simple font reaches here, and its codes are bytes.
|
||||
f.SetFallback(func(code int) (string, bool) {
|
||||
if r, ok := runeByName(program, code); ok {
|
||||
return string(r), true
|
||||
}
|
||||
if r, ok := runeByCharacterMap(program, maps, code); ok {
|
||||
return string(r), true
|
||||
}
|
||||
return "", false
|
||||
})
|
||||
}
|
||||
|
||||
// runeByName asks the program's own encoding what it calls a code, and reads
|
||||
// the character out of that name. Only a PostScript program — a Type 1 one, or
|
||||
// the CFF outlines of an OpenType font — carries such an encoding.
|
||||
func runeByName(program *opentype.Font, code int) (rune, bool) {
|
||||
gid, ok := program.GlyphIndexByCode(byte(code))
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
// A glyph the program does not name comes back as no name at all,
|
||||
// which names no character either.
|
||||
name, _ := program.GlyphName(gid)
|
||||
return pdffont.RuneOfGlyphName(name)
|
||||
}
|
||||
|
||||
// characterMaps says which of a program's cmap subtables are worth addressing,
|
||||
// by index, or -1 for one the program does not carry.
|
||||
type characterMaps struct {
|
||||
// symbol is the Microsoft Symbol subtable, platform 3 encoding 0: a font's
|
||||
// own codes, conventionally written at 0xF000 + code.
|
||||
symbol int
|
||||
// mac is the Macintosh Roman subtable, platform 1 encoding 0, indexed by
|
||||
// single bytes.
|
||||
mac int
|
||||
// unicode is a Unicode subtable — Microsoft Unicode, platform 3 encoding 1
|
||||
// or 10, or anything on platform 0 — indexed by codepoint. This is the one
|
||||
// that is inverted, because it is the only one whose codes are characters.
|
||||
unicode int
|
||||
}
|
||||
|
||||
// chooseCharacterMaps picks out the subtables a font is addressed through and
|
||||
// the one that says which character a glyph is for.
|
||||
//
|
||||
// A font can carry several subtables of a kind; the first of each is taken,
|
||||
// which is what a font that repeats one means by repeating it.
|
||||
func chooseCharacterMaps(program *opentype.Font) characterMaps {
|
||||
m := characterMaps{symbol: -1, mac: -1, unicode: -1}
|
||||
for i := range program.NumCharacterMaps() {
|
||||
platform, encoding, _, _ := program.CharacterMap(i)
|
||||
switch {
|
||||
case platform == 3 && encoding == 0:
|
||||
if m.symbol < 0 {
|
||||
m.symbol = i
|
||||
}
|
||||
case platform == 1 && encoding == 0:
|
||||
if m.mac < 0 {
|
||||
m.mac = i
|
||||
}
|
||||
case platform == 0, platform == 3 && (encoding == 1 || encoding == 10):
|
||||
if m.unicode < 0 {
|
||||
m.unicode = i
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// runeByCharacterMap walks a code through the font's own character map to a
|
||||
// glyph, and back out of its Unicode character map to the character that glyph
|
||||
// stands for.
|
||||
//
|
||||
// The way in is the one poppler uses for a font the document gave no encoding:
|
||||
// the Microsoft Symbol subtable if there is one, else the Macintosh Roman one,
|
||||
// addressed by the raw code and then, failing that, by 0xF000 + code, which is
|
||||
// where such subtables are conventionally written.
|
||||
//
|
||||
// The way out is the Unicode subtable, inverted. Without one there is no way
|
||||
// out: a glyph on its own says nothing about which character it is, and a
|
||||
// guess would be exactly the wrong letter the caller's guard exists to refuse.
|
||||
func runeByCharacterMap(program *opentype.Font, m characterMaps, code int) (rune, bool) {
|
||||
if m.unicode < 0 {
|
||||
return 0, false
|
||||
}
|
||||
in := m.symbol
|
||||
if in < 0 {
|
||||
in = m.mac
|
||||
}
|
||||
if in < 0 {
|
||||
return 0, false
|
||||
}
|
||||
gid, ok := program.GlyphIndexInMap(in, rune(code))
|
||||
if !ok {
|
||||
gid, ok = program.GlyphIndexInMap(in, rune(0xF000|code))
|
||||
}
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
r, ok := program.RuneOfGlyphInMap(m.unicode, gid)
|
||||
if !ok || !readableRune(r) {
|
||||
return 0, false
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
|
||||
// readableRune reports whether a character recovered this way is worth
|
||||
// reporting as text.
|
||||
//
|
||||
// A private-use codepoint is not: it means whatever the font decided it means
|
||||
// and nothing outside the font can read it, so a page full of them searches no
|
||||
// better than a page of nothing and looks, wrongly, like it was read. Control
|
||||
// characters are refused for the same reason — a page does not say them.
|
||||
func readableRune(r rune) bool {
|
||||
switch {
|
||||
case r < 0x20, r >= 0x7F && r <= 0x9F:
|
||||
return false
|
||||
case r >= 0xE000 && r <= 0xF8FF:
|
||||
return false
|
||||
case r >= 0xF0000:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// readProgram decodes an embedded font program. Which key it arrived under
|
||||
// says what it is: FontFile2 is TrueType, FontFile a PostScript Type 1
|
||||
// program, and FontFile3 either a bare CFF one or a whole OpenType font — the
|
||||
// key alone does not settle that one, so both are tried.
|
||||
func readProgram(key reader.Name, data []byte) (*opentype.Font, error) {
|
||||
switch key {
|
||||
case "FontFile":
|
||||
return opentype.ParseType1(data)
|
||||
case "FontFile3":
|
||||
if f, err := opentype.Parse(data); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
return opentype.ParseCFF(data)
|
||||
}
|
||||
return opentype.Parse(data)
|
||||
}
|
||||
Reference in New Issue
Block a user