Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ font addressed by character identifier, or — for a Type 3 font, whose
glyphs are little content streams — by running them. A simple font is
addressed by the name its encoding gives a code, and falls back on the
program's own built-in encoding when the document says nothing. A font
whose program cannot be read still advances the pen by its stated widths,
so what follows the text stays where it belongs.
whose program the file does not carry at all — two pages of the corpus in
five — is drawn in a **stand-in**: Arimo, Tinos and Cousine are
metric-compatible with Helvetica, Times and Courier, so the text comes out
the width it was meant to be even where the document gives no widths of its
own. A bold is made by stroking the outline as well as filling it and an
italic by leaning it over, since only one weight of each stand-in is carried.
Symbol and the dingbats are left alone: no face here has their glyphs, and
something is not better than nothing when the something is the wrong
alphabet.

Gradients and patterns: the **axial** and **radial** shadings that carry
nearly every gradient anyone draws, painted on their own with `sh` or used
Expand Down
26 changes: 26 additions & 0 deletions font.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ type pdfFont struct {

// fontMatrix is a Type 3 font's glyph space, as a transform.
fontMatrix geometry.Matrix

// substituted says the face is a stand-in rather than the font the
// document named, which is what happens when the document carries no
// font at all.
substituted bool
// embolden and slant say the document named a bold or an italic face and
// the stand-in is neither, so one is made of the other.
embolden, slant bool
}

// advance is how far the pen moves for one code, in text space.
//
// It is what the document says, when the document says anything. A font it
// does not carry may say nothing at all — one of the fourteen standard faces
// is written with no widths, since every reader is expected to know Times and
// Helvetica and Courier by heart — and then the stand-in's own advance is the
// answer, which is why the stand-ins are the three faces metric-compatible
// with those.
func (f *pdfFont) advance(code int) float64 {
if f.HasWidth(code) || !f.substituted || f.face == nil {
return f.Width(code)
}
return float64(f.face.AdvanceIndex(f.glyphIndex(code))) / f.perEm
}

// glyph is the outline of one code, in text space — a thousandth of the point
Expand Down Expand Up @@ -91,6 +114,9 @@ func (r *renderer) loadFont(dict reader.Dict) *pdfFont {
m := f.FontMatrix()
f.fontMatrix = geometry.Matrix{Xx: m[0], Xy: m[1], Yx: m[2], Yy: m[3], X0: m[4], Y0: m[5]}
r.attachProgram(f)
if f.program == nil {
r.attachStandIn(f)
}
return f
}

Expand Down
12 changes: 9 additions & 3 deletions fontodd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ func TestWidthsInShapesNobodyShouldWrite(t *testing.T) {
}
}

// TestFontProgramsThatCannotBeRead checks that a font the document does not
// carry, or carries in a form nothing here can read, is drawn in a stand-in
// rather than left off the page.
func TestFontProgramsThatCannotBeRead(t *testing.T) {
content := "BT /F 20 Tf 10 20 Td (A) Tj ET"
cases := []struct {
Expand Down Expand Up @@ -182,9 +185,12 @@ func TestFontProgramsThatCannotBeRead(t *testing.T) {
}
return font
})
// No outlines, so nothing is drawn — but nothing breaks either.
if inked(draw(t, d, Options{})) != 0 {
t.Errorf("%s: something was drawn", c.name)
// A font whose program cannot be read is drawn in a stand-in, which
// is what every reader does: two pages of the corpus in five carry
// no font at all, and drawing nothing for those would leave them
// with no text on them.
if inked(draw(t, d, Options{})) == 0 {
t.Errorf("%s: nothing was drawn", c.name)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ go 1.26.4

require (
github.com/go-gfx/gfx v0.10.0
github.com/go-opentype/fonts v0.8.0
github.com/go-opentype/fonts v0.9.0
github.com/go-opentype/opentype v0.9.0
github.com/go-pdfkit/reader v0.4.0
)

require github.com/go-pdfkit/pdffont v0.1.0 // indirect
require github.com/go-pdfkit/pdffont v0.2.0
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
github.com/go-gfx/gfx v0.10.0 h1:3AqOO8TZph6/U8+ejJxYkCZ+wzddxAbZ7fxi6TvGop4=
github.com/go-gfx/gfx v0.10.0/go.mod h1:bFt/MWyYWRU3Ic9IaB8XOC9KLMMHRRmahMk4FaIGK7g=
github.com/go-opentype/fonts v0.8.0 h1:77i3VPIH90GbstzNb21mk+an4WvEOe2idC6W+J0n0fw=
github.com/go-opentype/fonts v0.8.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g=
github.com/go-opentype/fonts v0.9.0 h1:slB6OB3riLyUPrOxqXe0s6/AzdenF1TDvCN8N87hhQk=
github.com/go-opentype/fonts v0.9.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g=
github.com/go-opentype/opentype v0.9.0 h1:GFgcJ3nwTDp4NJr5O+Paw7lhZx5Jv/R+noZwvhYDlkM=
github.com/go-opentype/opentype v0.9.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU=
github.com/go-pdfkit/pdffont v0.1.0 h1:mThWvPn3LLETup2adm9xMcNZWECP2Wq3eTSyTW4O3I0=
github.com/go-pdfkit/pdffont v0.1.0/go.mod h1:y4vo5DgT95e57C3XxIWfA/xss+x6RwZwyj6KWdJc86s=
github.com/go-pdfkit/reader v0.3.0 h1:/r7aCt6jdOdQ7hYrnnMLxc2NuCjPfROEEy73w1O94eM=
github.com/go-pdfkit/reader v0.3.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=
github.com/go-pdfkit/pdffont v0.2.0 h1:yAp/oR5Z2kkqs4r0GWMalZMC7rc7XSCZXgwIypbpMWM=
github.com/go-pdfkit/pdffont v0.2.0/go.mod h1:y4vo5DgT95e57C3XxIWfA/xss+x6RwZwyj6KWdJc86s=
github.com/go-pdfkit/reader v0.4.0 h1:qPbNZSO+Xl+4NBvQoV1PYt7HlqHaEAQ1tUc6/IL8JAU=
github.com/go-pdfkit/reader v0.4.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=
160 changes: 160 additions & 0 deletions substitute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package render

import (
"strings"
"sync"

"github.com/go-opentype/fonts/arimo"
"github.com/go-opentype/fonts/cousine"
"github.com/go-opentype/fonts/tinos"
"github.com/go-opentype/opentype"
"github.com/go-pdfkit/pdffont"
"github.com/go-pdfkit/reader"
)

// A PDF need not carry the font its text is set in. Fourteen faces — four
// weights each of Helvetica, Times and Courier, plus Symbol and
// ZapfDingbats — every reader is expected to have of its own, and in practice
// a file names any font at all and hopes.
//
// Of 95 818 corpus pages that show text, 40 437 name a font the file does not
// carry and 38 663 carry none at all. Drawing nothing for those is not an
// option: it is two pages in five with no text on them.
//
// So a stand-in is drawn. Arimo, Tinos and Cousine are metric-compatible with
// Helvetica, Times and Courier — the same advances, glyph for glyph — so text
// set in one of the standard faces comes out the width it was meant to be even
// where the document gives no widths of its own. For any other name the
// descriptor's own flags say whether it wanted a serif, a sans or a typewriter,
// and one of the three is drawn in its place.
//
// What this cannot do is invent a font's shapes. A page set in a face nobody
// has is a page set in something else, and it says so nowhere: that is what
// every reader does, and it is better than a blank page by the width of the
// text on it.
type substitute struct {
once sync.Once
ttf []byte
font *opentype.Font
err error
}

// The three stand-ins, each read once and shared.
var (
sansStandIn = &substitute{ttf: arimo.TTF}
serifStandIn = &substitute{ttf: tinos.TTF}
monoStandIn = &substitute{ttf: cousine.TTF}
)

// get parses the stand-in, once.
func (s *substitute) get() (*opentype.Font, error) {
s.once.Do(func() { s.font, s.err = opentype.Parse(s.ttf) })
return s.font, s.err
}

// The flags a font descriptor carries that say what a face looks like.
const (
flagFixedPitch = 1 << 0
flagSerif = 1 << 1
flagItalic = 1 << 6
)

// standIn picks the face to draw a font that carries no program of its own.
func (r *renderer) standIn(f *pdffont.Font) *substitute {
name := strings.ToLower(baseFontName(r.doc, f))
switch {
case strings.Contains(name, "courier") || strings.Contains(name, "mono"):
return monoStandIn
case strings.Contains(name, "times") || strings.Contains(name, "roman") ||
strings.Contains(name, "serif") || strings.Contains(name, "georgia") ||
strings.Contains(name, "garamond") || strings.Contains(name, "book"):
// "DejaVu Sans" holds "sans" and would be caught below; the serif
// names are checked first because "DejaVu Serif" holds neither
// "times" nor "roman".
return serifStandIn
case strings.Contains(name, "helvetica") || strings.Contains(name, "arial") ||
strings.Contains(name, "sans"):
return sansStandIn
}
// Nothing in the name said: the descriptor's flags are what is left.
flags, _ := reader.ToInt(resolve(r.doc, f.Descriptor().Get("Flags")))
switch {
case flags&flagFixedPitch != 0:
return monoStandIn
case flags&flagSerif != 0:
return serifStandIn
}
return sansStandIn
}

// baseFontName is what the document calls the font, without the six-letter tag
// a subsetted one carries in front of it.
func baseFontName(d *reader.Document, f *pdffont.Font) string {
name, _ := reader.ToName(resolve(d, f.Dict().Get("BaseFont")))
s := string(name)
if len(s) > 7 && s[6] == '+' {
return s[7:]
}
return s
}

// attachStandIn gives a font with no program of its own a face to be drawn
// with, and leaves it alone when there is nothing sensible to draw: a
// composite font is addressed by glyph number, and a stand-in's glyph numbers
// are its own, so drawing one would put arbitrary letters on the page.
//
// Symbol and ZapfDingbats are left alone for the same reason: no face here
// carries their glyphs, and something is not better than nothing when the
// something is the wrong alphabet.
func (r *renderer) attachStandIn(f *pdfFont) {
if f.Kind() == pdffont.Composite || f.Kind() == pdffont.Type3 {
return
}
switch strings.ToLower(baseFontName(r.doc, f.Font)) {
case "symbol", "zapfdingbats", "dingbats":
return
}
program, err := r.standIn(f.Font).get()
if err != nil {
return
}
f.program = program
f.perEm = float64(program.UnitsPerEm())
f.face = program.NewFace(program.UnitsPerEm())
f.substituted = true
f.embolden, f.slant = r.wantsBoldItalic(f.Font)
}

// wantsBoldItalic reads whether the font the document named was a bold or an
// italic one. Only one weight of each stand-in is carried, so a bold is drawn
// by stroking the outline as well as filling it and an italic by leaning it
// over — which is what a typesetter calls a faux bold and a faux italic, and
// what every reader does with a face it has not got in the weight asked for.
func (r *renderer) wantsBoldItalic(f *pdffont.Font) (embolden, slant bool) {
name := strings.ToLower(baseFontName(r.doc, f))
embolden = strings.Contains(name, "bold") || strings.Contains(name, "black") ||
strings.Contains(name, "heavy") || strings.Contains(name, "semibold")
slant = strings.Contains(name, "italic") || strings.Contains(name, "oblique")
if !slant {
flags, _ := reader.ToInt(resolve(r.doc, f.Descriptor().Get("Flags")))
slant = flags&flagItalic != 0
}
if !embolden {
// A stem width past about a hundred and sixty thousandths is a bold
// face; the descriptor is the only place a document says so in a
// number rather than in a name.
if w, ok := reader.ToFloat(resolve(r.doc, f.Descriptor().Get("StemV"))); ok && w >= 120 {
embolden = true
}
}
return embolden, slant
}

// faux is how far a faux bold is stroked, as a fraction of the em. It is what
// a typesetter would reach for: enough to read as bold beside the same face
// unemboldened, not so much that the counters fill in.
const fauxBoldWidth = 0.024

// fauxSlant is how far a faux italic leans, as a tangent: about twelve
// degrees, which is where most italics sit.
const fauxSlant = 0.21
Loading
Loading