oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package fonts_test
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/carlosframework/docs/internal/fonts"
8)
9
10func TestEveryFaceParses(t *testing.T) {
11 all := fonts.All()
12 if len(all) != 12 {
13 t.Fatalf("want 12 faces (3 families x 4 styles), got %d", len(all))
14 }
15 for _, face := range all {
16 font, err := face.Parsed()
17 if err != nil {
18 t.Fatalf("%s: %v", face.File, err)
19 }
20 if font.UnitsPerEm == 0 || font.NumGlyphs == 0 {
21 t.Errorf("%s: unitsPerEm=%d numGlyphs=%d", face.File, font.UnitsPerEm, font.NumGlyphs)
22 }
23 if font.Ascender <= 0 || font.Descender >= 0 {
24 t.Errorf("%s: implausible vertical metrics asc=%d desc=%d", face.File, font.Ascender, font.Descender)
25 }
26 // Every face must be able to set ordinary prose. A face whose
27 // cmap this build failed to read still parses, so the check
28 // that matters is that letters resolve to real glyphs.
29 for _, r := range "The quick brown fox, 0123456789 — “quoted” café" {
30 if font.GlyphIndex(r) == 0 {
31 t.Errorf("%s: no glyph for %q", face.File, r)
32 }
33 }
34 }
35}
36
37// Advance widths are what every measurement in internal/pdf is built
38// on. A monospace face is the one where a wrong answer is unmissable:
39// every glyph must advance by the same amount.
40func TestMonoAdvancesAreUniform(t *testing.T) {
41 font, err := fonts.Get(fonts.Mono, false, false).Parsed()
42 if err != nil {
43 t.Fatal(err)
44 }
45 want := font.Advance(font.GlyphIndex('m'))
46 if want == 0 {
47 t.Fatal("no advance for 'm'")
48 }
49 for _, r := range "il.MW@" {
50 if got := font.Advance(font.GlyphIndex(r)); got != want {
51 t.Errorf("mono advance for %q = %d, want %d", r, got, want)
52 }
53 }
54}
55
56// The proportional faces must NOT be uniform — the same assertion the
57// other way round, so a bug that returned one constant width for
58// everything cannot pass both tests.
59func TestProportionalAdvancesDiffer(t *testing.T) {
60 font, err := fonts.Get(fonts.Serif, false, false).Parsed()
61 if err != nil {
62 t.Fatal(err)
63 }
64 narrow := font.Advance(font.GlyphIndex('i'))
65 wide := font.Advance(font.GlyphIndex('W'))
66 if narrow == 0 || wide == 0 || narrow >= wide {
67 t.Errorf("serif advances: i=%d W=%d, want 0 < i < W", narrow, wide)
68 }
69}
70
71func TestWidthEmScalesWithTheString(t *testing.T) {
72 font, err := fonts.Get(fonts.Serif, false, false).Parsed()
73 if err != nil {
74 t.Fatal(err)
75 }
76 one, gids := font.WidthEm("n")
77 two, _ := font.WidthEm("nn")
78 if len(gids) != 1 {
79 t.Errorf("WidthEm returned %d gids for one rune", len(gids))
80 }
81 if one <= 0 {
82 t.Fatalf("width of %q is %v", "n", one)
83 }
84 if diff := two - 2*one; diff > 1e-9 || diff < -1e-9 {
85 t.Errorf("width(nn)=%v, want 2*width(n)=%v", two, 2*one)
86 }
87}
88
89// The subset has to be a font a reader can actually use, so this
90// parses the output rather than checking that it got smaller. Glyph
91// ids must survive unchanged (the whole design — see truetype.go), the
92// kept glyphs must still have outlines, and a dropped glyph must be
93// empty rather than missing.
94func TestSubsetKeepsGlyphIdsAndOutlines(t *testing.T) {
95 face := fonts.Get(fonts.Serif, false, false)
96 font, err := face.Parsed()
97 if err != nil {
98 t.Fatal(err)
99 }
100
101 keep := map[uint16]bool{}
102 const sample = "Hello"
103 for _, r := range sample {
104 keep[font.GlyphIndex(r)] = true
105 }
106
107 sub, err := font.Subset(keep)
108 if err != nil {
109 t.Fatal(err)
110 }
111 if len(sub) >= len(face.Data) {
112 t.Errorf("subset is %d bytes, original is %d — not a subset", len(sub), len(face.Data))
113 }
114
115 reparsed, err := fonts.Parse(sub)
116 if err != nil {
117 t.Fatalf("the subset does not parse: %v", err)
118 }
119 if reparsed.NumGlyphs != font.NumGlyphs {
120 t.Errorf("subset numGlyphs=%d, want %d unchanged", reparsed.NumGlyphs, font.NumGlyphs)
121 }
122 if reparsed.UnitsPerEm != font.UnitsPerEm {
123 t.Errorf("subset unitsPerEm=%d, want %d", reparsed.UnitsPerEm, font.UnitsPerEm)
124 }
125 for _, r := range sample {
126 gid := font.GlyphIndex(r)
127 if got := reparsed.Advance(gid); got != font.Advance(gid) {
128 t.Errorf("advance for %q changed: %d -> %d", r, font.Advance(gid), got)
129 }
130 if !glyphHasOutline(t, sub, gid) {
131 t.Errorf("kept glyph for %q has no outline in the subset", r)
132 }
133 }
134
135 // A glyph nobody asked for must be gone. 'Z' is not in "Hello".
136 if gid := font.GlyphIndex('Z'); gid != 0 && glyphHasOutline(t, sub, gid) {
137 t.Errorf("glyph for 'Z' survived a subset that did not ask for it")
138 }
139}
140
141// The hinting tables travel with the glyphs. ISO 32000-1 §9.9 requires
142// cvt /fpgm/prep to be in an embedded font whose instructions need
143// them, and Liberation's glyph programs call functions defined in fpgm
144// and read values from cvt — so a subset without them is executing
145// instructions against definitions that are not in the file.
146func TestSubsetKeepsTheHintingTables(t *testing.T) {
147 face := fonts.Get(fonts.Serif, false, false)
148 font, err := face.Parsed()
149 if err != nil {
150 t.Fatal(err)
151 }
152 sub, err := font.Subset(map[uint16]bool{font.GlyphIndex('A'): true})
153 if err != nil {
154 t.Fatal(err)
155 }
156 for _, tag := range []string{"cvt ", "fpgm", "prep"} {
157 if !fontHasTable(t, sub, tag) {
158 t.Errorf("the subset dropped %q, which its glyph instructions need", tag)
159 }
160 }
161 // And it is still a subset: the hinting tables are tens of
162 // kilobytes, not hundreds.
163 if len(sub) >= len(face.Data)/2 {
164 t.Errorf("subset is %d bytes against a %d-byte face — the hinting tables should not cost that much",
165 len(sub), len(face.Data))
166 }
167}
168
169// fontHasTable reports whether a built font file carries a table.
170func fontHasTable(t *testing.T, file []byte, tag string) bool {
171 t.Helper()
172 if len(file) < 12 {
173 t.Fatal("not a font")
174 }
175 n := int(file[4])<<8 | int(file[5])
176 for i := 0; i < n; i++ {
177 rec := 12 + i*16
178 if rec+4 > len(file) {
179 break
180 }
181 if string(file[rec:rec+4]) == tag {
182 return true
183 }
184 }
185 return false
186}
187
188// A composite glyph's components must come along, or the accent
189// renders as nothing.
190func TestSubsetFollowsCompositeComponents(t *testing.T) {
191 face := fonts.Get(fonts.Serif, false, false)
192 font, err := face.Parsed()
193 if err != nil {
194 t.Fatal(err)
195 }
196 gid := font.GlyphIndex('é')
197 if gid == 0 {
198 t.Skip("no é in this face")
199 }
200 sub, err := font.Subset(map[uint16]bool{gid: true})
201 if err != nil {
202 t.Fatal(err)
203 }
204 if !glyphHasOutline(t, sub, gid) {
205 t.Fatal("é itself is missing from its own subset")
206 }
207 // 'e' was never asked for; it must still be present, because é is
208 // assembled from it. (If this face happens to draw é as a simple
209 // glyph, the assertion still holds trivially — é has an outline
210 // either way — so this cannot pass by accident on a face where
211 // there is nothing to follow.)
212 base := font.GlyphIndex('e')
213 if isComposite(t, face.Data, font, gid) && !glyphHasOutline(t, sub, base) {
214 t.Error("é is composite but its base glyph was dropped from the subset")
215 }
216}
217
218// glyphHasOutline reports whether gid occupies bytes in the file's
219// glyf table — the actual question a reader asks when it draws.
220func glyphHasOutline(t *testing.T, file []byte, gid uint16) bool {
221 t.Helper()
222 f, err := fonts.Parse(file)
223 if err != nil {
224 t.Fatalf("parse: %v", err)
225 }
226 return f.GlyphLen(gid) > 0
227}
228
229func isComposite(t *testing.T, file []byte, orig *fonts.Font, gid uint16) bool {
230 t.Helper()
231 f, err := fonts.Parse(file)
232 if err != nil {
233 t.Fatalf("parse: %v", err)
234 }
235 return f.GlyphIsComposite(gid)
236}
237
238// Every face is served at a content-hashed path, so the URL changes
239// whenever the bytes do and the year-long cache header is safe.
240func TestFacePathsAreUniqueAndHashed(t *testing.T) {
241 seen := map[string]bool{}
242 for _, f := range fonts.All() {
243 if !strings.HasPrefix(f.Path, fonts.PathPrefix) || !strings.HasSuffix(f.Path, ".ttf") {
244 t.Errorf("%s: unexpected path %q", f.File, f.Path)
245 }
246 if seen[f.Path] {
247 t.Errorf("duplicate path %q", f.Path)
248 }
249 seen[f.Path] = true
250 got, ok := fonts.Asset(f.Path)
251 if !ok || got.File != f.File {
252 t.Errorf("Asset(%q) did not round-trip to %s", f.Path, f.File)
253 }
254 }
255}
256
257func TestLicenceTravelsWithTheFonts(t *testing.T) {
258 l := fonts.Licence()
259 if !strings.Contains(l, "SIL OPEN FONT LICENSE Version 1.1") {
260 t.Error("the OFL text is not what is being served")
261 }
262 if strings.Contains(l, "GNU General Public License") {
263 t.Error("the Debian packaging licence leaked into the font licence file")
264 }
265}
266
267func TestFamilyKeysRoundTrip(t *testing.T) {
268 for _, fam := range []fonts.Family{fonts.Serif, fonts.Sans, fonts.Mono} {
269 got, ok := fonts.FamilyForKey(fam.Key())
270 if !ok || got != fam {
271 t.Errorf("%q did not round-trip", fam.Key())
272 }
273 }
274 if _, ok := fonts.FamilyForKey("comic"); ok {
275 t.Error("an unknown family key must be a miss, not a silent serif")
276 }
277}
278