package fonts_test import ( "strings" "testing" "github.com/carlosframework/docs/internal/fonts" ) func TestEveryFaceParses(t *testing.T) { all := fonts.All() if len(all) != 12 { t.Fatalf("want 12 faces (3 families x 4 styles), got %d", len(all)) } for _, face := range all { font, err := face.Parsed() if err != nil { t.Fatalf("%s: %v", face.File, err) } if font.UnitsPerEm == 0 || font.NumGlyphs == 0 { t.Errorf("%s: unitsPerEm=%d numGlyphs=%d", face.File, font.UnitsPerEm, font.NumGlyphs) } if font.Ascender <= 0 || font.Descender >= 0 { t.Errorf("%s: implausible vertical metrics asc=%d desc=%d", face.File, font.Ascender, font.Descender) } // Every face must be able to set ordinary prose. A face whose // cmap this build failed to read still parses, so the check // that matters is that letters resolve to real glyphs. for _, r := range "The quick brown fox, 0123456789 — “quoted” café" { if font.GlyphIndex(r) == 0 { t.Errorf("%s: no glyph for %q", face.File, r) } } } } // Advance widths are what every measurement in internal/pdf is built // on. A monospace face is the one where a wrong answer is unmissable: // every glyph must advance by the same amount. func TestMonoAdvancesAreUniform(t *testing.T) { font, err := fonts.Get(fonts.Mono, false, false).Parsed() if err != nil { t.Fatal(err) } want := font.Advance(font.GlyphIndex('m')) if want == 0 { t.Fatal("no advance for 'm'") } for _, r := range "il.MW@" { if got := font.Advance(font.GlyphIndex(r)); got != want { t.Errorf("mono advance for %q = %d, want %d", r, got, want) } } } // The proportional faces must NOT be uniform — the same assertion the // other way round, so a bug that returned one constant width for // everything cannot pass both tests. func TestProportionalAdvancesDiffer(t *testing.T) { font, err := fonts.Get(fonts.Serif, false, false).Parsed() if err != nil { t.Fatal(err) } narrow := font.Advance(font.GlyphIndex('i')) wide := font.Advance(font.GlyphIndex('W')) if narrow == 0 || wide == 0 || narrow >= wide { t.Errorf("serif advances: i=%d W=%d, want 0 < i < W", narrow, wide) } } func TestWidthEmScalesWithTheString(t *testing.T) { font, err := fonts.Get(fonts.Serif, false, false).Parsed() if err != nil { t.Fatal(err) } one, gids := font.WidthEm("n") two, _ := font.WidthEm("nn") if len(gids) != 1 { t.Errorf("WidthEm returned %d gids for one rune", len(gids)) } if one <= 0 { t.Fatalf("width of %q is %v", "n", one) } if diff := two - 2*one; diff > 1e-9 || diff < -1e-9 { t.Errorf("width(nn)=%v, want 2*width(n)=%v", two, 2*one) } } // The subset has to be a font a reader can actually use, so this // parses the output rather than checking that it got smaller. Glyph // ids must survive unchanged (the whole design — see truetype.go), the // kept glyphs must still have outlines, and a dropped glyph must be // empty rather than missing. func TestSubsetKeepsGlyphIdsAndOutlines(t *testing.T) { face := fonts.Get(fonts.Serif, false, false) font, err := face.Parsed() if err != nil { t.Fatal(err) } keep := map[uint16]bool{} const sample = "Hello" for _, r := range sample { keep[font.GlyphIndex(r)] = true } sub, err := font.Subset(keep) if err != nil { t.Fatal(err) } if len(sub) >= len(face.Data) { t.Errorf("subset is %d bytes, original is %d — not a subset", len(sub), len(face.Data)) } reparsed, err := fonts.Parse(sub) if err != nil { t.Fatalf("the subset does not parse: %v", err) } if reparsed.NumGlyphs != font.NumGlyphs { t.Errorf("subset numGlyphs=%d, want %d unchanged", reparsed.NumGlyphs, font.NumGlyphs) } if reparsed.UnitsPerEm != font.UnitsPerEm { t.Errorf("subset unitsPerEm=%d, want %d", reparsed.UnitsPerEm, font.UnitsPerEm) } for _, r := range sample { gid := font.GlyphIndex(r) if got := reparsed.Advance(gid); got != font.Advance(gid) { t.Errorf("advance for %q changed: %d -> %d", r, font.Advance(gid), got) } if !glyphHasOutline(t, sub, gid) { t.Errorf("kept glyph for %q has no outline in the subset", r) } } // A glyph nobody asked for must be gone. 'Z' is not in "Hello". if gid := font.GlyphIndex('Z'); gid != 0 && glyphHasOutline(t, sub, gid) { t.Errorf("glyph for 'Z' survived a subset that did not ask for it") } } // The hinting tables travel with the glyphs. ISO 32000-1 §9.9 requires // cvt /fpgm/prep to be in an embedded font whose instructions need // them, and Liberation's glyph programs call functions defined in fpgm // and read values from cvt — so a subset without them is executing // instructions against definitions that are not in the file. func TestSubsetKeepsTheHintingTables(t *testing.T) { face := fonts.Get(fonts.Serif, false, false) font, err := face.Parsed() if err != nil { t.Fatal(err) } sub, err := font.Subset(map[uint16]bool{font.GlyphIndex('A'): true}) if err != nil { t.Fatal(err) } for _, tag := range []string{"cvt ", "fpgm", "prep"} { if !fontHasTable(t, sub, tag) { t.Errorf("the subset dropped %q, which its glyph instructions need", tag) } } // And it is still a subset: the hinting tables are tens of // kilobytes, not hundreds. if len(sub) >= len(face.Data)/2 { t.Errorf("subset is %d bytes against a %d-byte face — the hinting tables should not cost that much", len(sub), len(face.Data)) } } // fontHasTable reports whether a built font file carries a table. func fontHasTable(t *testing.T, file []byte, tag string) bool { t.Helper() if len(file) < 12 { t.Fatal("not a font") } n := int(file[4])<<8 | int(file[5]) for i := 0; i < n; i++ { rec := 12 + i*16 if rec+4 > len(file) { break } if string(file[rec:rec+4]) == tag { return true } } return false } // A composite glyph's components must come along, or the accent // renders as nothing. func TestSubsetFollowsCompositeComponents(t *testing.T) { face := fonts.Get(fonts.Serif, false, false) font, err := face.Parsed() if err != nil { t.Fatal(err) } gid := font.GlyphIndex('é') if gid == 0 { t.Skip("no é in this face") } sub, err := font.Subset(map[uint16]bool{gid: true}) if err != nil { t.Fatal(err) } if !glyphHasOutline(t, sub, gid) { t.Fatal("é itself is missing from its own subset") } // 'e' was never asked for; it must still be present, because é is // assembled from it. (If this face happens to draw é as a simple // glyph, the assertion still holds trivially — é has an outline // either way — so this cannot pass by accident on a face where // there is nothing to follow.) base := font.GlyphIndex('e') if isComposite(t, face.Data, font, gid) && !glyphHasOutline(t, sub, base) { t.Error("é is composite but its base glyph was dropped from the subset") } } // glyphHasOutline reports whether gid occupies bytes in the file's // glyf table — the actual question a reader asks when it draws. func glyphHasOutline(t *testing.T, file []byte, gid uint16) bool { t.Helper() f, err := fonts.Parse(file) if err != nil { t.Fatalf("parse: %v", err) } return f.GlyphLen(gid) > 0 } func isComposite(t *testing.T, file []byte, orig *fonts.Font, gid uint16) bool { t.Helper() f, err := fonts.Parse(file) if err != nil { t.Fatalf("parse: %v", err) } return f.GlyphIsComposite(gid) } // Every face is served at a content-hashed path, so the URL changes // whenever the bytes do and the year-long cache header is safe. func TestFacePathsAreUniqueAndHashed(t *testing.T) { seen := map[string]bool{} for _, f := range fonts.All() { if !strings.HasPrefix(f.Path, fonts.PathPrefix) || !strings.HasSuffix(f.Path, ".ttf") { t.Errorf("%s: unexpected path %q", f.File, f.Path) } if seen[f.Path] { t.Errorf("duplicate path %q", f.Path) } seen[f.Path] = true got, ok := fonts.Asset(f.Path) if !ok || got.File != f.File { t.Errorf("Asset(%q) did not round-trip to %s", f.Path, f.File) } } } func TestLicenceTravelsWithTheFonts(t *testing.T) { l := fonts.Licence() if !strings.Contains(l, "SIL OPEN FONT LICENSE Version 1.1") { t.Error("the OFL text is not what is being served") } if strings.Contains(l, "GNU General Public License") { t.Error("the Debian packaging licence leaked into the font licence file") } } func TestFamilyKeysRoundTrip(t *testing.T) { for _, fam := range []fonts.Family{fonts.Serif, fonts.Sans, fonts.Mono} { got, ok := fonts.FamilyForKey(fam.Key()) if !ok || got != fam { t.Errorf("%q did not round-trip", fam.Key()) } } if _, ok := fonts.FamilyForKey("comic"); ok { t.Error("an unknown family key must be a miss, not a silent serif") } }