package pdf import "core:mem" import "core:mem/virtual" import "core:testing" import "core:time" // --------------------------------------------------------------------------- // Fixture and font assembly // // Every font under test is built in a local arena, so a failing assertion // cannot leak: the arena is destroyed on the way out whatever happened. // // The tables themselves are byte literals -- the bytes are the thing under // test -- and only the scaffolding around them is assembled: the sfnt table // directory, whose layout is uninteresting, and the `head`/`maxp`/`hhea` // headers, which exist only to carry three numbers each. // --------------------------------------------------------------------------- @(private="file") Tt_Fixture :: struct { arena: virtual.Arena, alloc: mem.Allocator, } @(private="file") ttf_init :: proc(f: ^Tt_Fixture) { _ = virtual.arena_init_growing(&f.arena) f.alloc = virtual.arena_allocator(&f.arena) } @(private="file") ttf_destroy :: proc(f: ^Tt_Fixture) { virtual.arena_destroy(&f.arena) } @(private="file") Tt_Table :: struct { tag: string, data: []byte, } // ttf_font writes a table directory around `tables`, in the order given. That // order is deliberately not sorted by tag in most tests: the format requires // sorted records and real files disobey, so the reader must not depend on it. @(private="file") ttf_font :: proc(f: ^Tt_Fixture, tables: []Tt_Table, sfnt: u32 = 0x00010000) -> []byte { total := 12 + 16 * len(tables) for t in tables { total += (len(t.data) + 3) & ~int(3) } out := make([]byte, total, f.alloc) put32 :: proc(b: []byte, off: int, v: u32) { b[off] = u8(v >> 24) b[off + 1] = u8(v >> 16) b[off + 2] = u8(v >> 8) b[off + 3] = u8(v) } put32(out, 0, sfnt) out[4] = u8(len(tables) >> 8) out[5] = u8(len(tables)) pos := 12 + 16 * len(tables) for t, i in tables { rec := 12 + 16 * i for k in 0 ..< 4 { out[rec + k] = t.tag[k] if k < len(t.tag) else ' ' } put32(out, rec + 8, u32(pos)) put32(out, rec + 12, u32(len(t.data))) copy(out[pos:], t.data) pos += (len(t.data) + 3) & ~int(3) } return out } @(private="file") ttf_bytes :: proc(f: ^Tt_Fixture, vals: ..u8) -> []byte { out := make([]byte, len(vals), f.alloc) copy(out, vals) return out } // ttf_head is the 54-byte header, of which only unitsPerEm (18) and // indexToLocFormat (50) are read. @(private="file") ttf_head :: proc(f: ^Tt_Fixture, upem: u16, long_loca: bool) -> []byte { out := make([]byte, 54, f.alloc) out[18] = u8(upem >> 8) out[19] = u8(upem) out[51] = 1 if long_loca else 0 return out } @(private="file") ttf_maxp :: proc(f: ^Tt_Fixture, num_glyphs: u16) -> []byte { out := make([]byte, 32, f.alloc) out[0] = 0x00 out[1] = 0x01 out[4] = u8(num_glyphs >> 8) out[5] = u8(num_glyphs) return out } // ttf_hhea is the 36-byte header whose last field is numberOfHMetrics. @(private="file") ttf_hhea :: proc(f: ^Tt_Fixture, n: u16) -> []byte { out := make([]byte, 36, f.alloc) out[34] = u8(n >> 8) out[35] = u8(n) return out } @(private="file") ttf_loca_short :: proc(f: ^Tt_Fixture, offs: ..u16) -> []byte { out := make([]byte, 2 * len(offs), f.alloc) for v, i in offs { out[2 * i] = u8(v >> 8) out[2 * i + 1] = u8(v) } return out } @(private="file") ttf_loca_long :: proc(f: ^Tt_Fixture, offs: ..u32) -> []byte { out := make([]byte, 4 * len(offs), f.alloc) for v, i in offs { out[4 * i] = u8(v >> 24) out[4 * i + 1] = u8(v >> 16) out[4 * i + 2] = u8(v >> 8) out[4 * i + 3] = u8(v) } return out } @(private="file") Tt_Cmap_Rec :: struct { plat, enc: u16, sub: []byte, } // ttf_cmap wraps subtables in a cmap header: version, count, and one // (platform, encoding, offset) record each. @(private="file") ttf_cmap :: proc(f: ^Tt_Fixture, recs: ..Tt_Cmap_Rec) -> []byte { total := 4 + 8 * len(recs) for r in recs { total += len(r.sub) } out := make([]byte, total, f.alloc) out[3] = u8(len(recs)) // version 0, numTables pos := 4 + 8 * len(recs) for r, i in recs { rec := 4 + 8 * i out[rec] = u8(r.plat >> 8) out[rec + 1] = u8(r.plat) out[rec + 2] = u8(r.enc >> 8) out[rec + 3] = u8(r.enc) out[rec + 4] = u8(pos >> 24) out[rec + 5] = u8(pos >> 16) out[rec + 6] = u8(pos >> 8) out[rec + 7] = u8(pos) copy(out[pos:], r.sub) pos += len(r.sub) } return out } // --------------------------------------------------------------------------- // Glyph programs, as byte literals // --------------------------------------------------------------------------- // GLYF_SQUARE is one contour of four on-curve points at (0,0) (100,0) // (100,100) (0,100), 22 bytes. Its flags exercise every coordinate encoding // there is: SAME (a zero delta with no byte at all), a positive short vector, // and a signed 16-bit delta. @(private="file") GLYF_SQUARE := [?]u8{ 0x00, 0x01, // numberOfContours = 1 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64, // bbox 0 0 100 100 0x00, 0x03, // endPtsOfContours = {3} 0x00, 0x00, // instructionLength = 0 0x31, // on, x same, y same 0x33, // on, x short +, y same 0x35, // on, x same, y short + 0x21, // on, x word, y same 0x64, 0xFF, 0x9C, // x: +100, then -100 as a word 0x64, // y: +100 } // GLYF_QUAD has an on-curve point, two consecutive off-curve points -- which // imply an on-curve point at their midpoint (100,100) -- and a closing // on-curve point. Points: (0,0) (50,100)off (150,100)off (100,0). Its last // point is reached by a negative short vector in both axes. 24 bytes, the last // one padding. @(private="file") GLYF_QUAD := [?]u8{ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x00, 0x64, // bbox 0 0 150 100 0x00, 0x03, 0x00, 0x00, 0x31, // on, x same, y same 0x36, // off, x short +, y short + 0x32, // off, x short +, y same 0x07, // on, x short -, y short - 0x32, 0x64, 0x32, // x: +50, +100, -50 0x64, 0x64, // y: +100, -100 0x00, } // GLYF_LINE is two points whose x deltas are both signed words and whose y // deltas are both the SAME encoding, so the last byte of the glyph belongs to // a coordinate: the shape a truncation sweep needs to reach the 16-bit read. @(private="file") GLYF_LINE := [?]u8{ 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, 0x01, 0x90, 0x00, 0x00, // bbox 100 0 400 0 0x00, 0x01, // endPtsOfContours = {1} 0x00, 0x00, 0x21, 0x21, // on, x word, y same 0x00, 0x64, 0x01, 0x2C, // x: +100, +300 } // GLYF_REPEAT is four on-curve points written with the REPEAT flag: one flag // byte covers all four. Deltas (10,10) (20,0) (0,20) (30,30), so the points are // (10,10) (30,10) (30,30) (60,60). @(private="file") GLYF_REPEAT := [?]u8{ 0x00, 0x01, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x03, 0x00, 0x00, 0x3F, 0x03, // on, both short +, REPEAT 3 0x0A, 0x14, 0x00, 0x1E, // x deltas 0x0A, 0x00, 0x14, 0x1E, // y deltas } // GLYF_CONTOURS has three contour end indices, the second and third of which // go backwards: {5, 2, 5} over six points. Only the first is a contour; the // other two must not re-cut points that already belong to it. @(private="file") GLYF_CONTOURS := [?]u8{ 0x00, 0x03, // numberOfContours = 3 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x05, 0x00, 0x02, 0x00, 0x05, // endPtsOfContours 0x00, 0x00, 0x3F, 0x05, // on, both short +, REPEAT 5 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, // x deltas 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, // y deltas } // GLYF_CIRCLE is a contour with no on-curve point at all: four control points // at the corners of a square. Legal, and produced by real fonts. @(private="file") GLYF_CIRCLE := [?]u8{ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00, 0x30, // off, x same, y same 0x32, // off, x short +, y same 0x34, // off, x same, y short + 0x20, // off, x word, y same 0x64, 0xFF, 0x9C, 0x64, } // --------------------------------------------------------------------------- // Assertions // --------------------------------------------------------------------------- @(private="file") ttf_expect_path :: proc(t: ^testing.T, p: ^Path, verbs: []Path_Verb, pts: []Point, loc := #caller_location) { if !testing.expectf(t, len(p.verbs) == len(verbs), "%v verbs, wanted %v", p.verbs[:], verbs, loc = loc) { return } for v, i in verbs { if !testing.expectf(t, p.verbs[i] == v, "verb %d is %v, wanted %v (%v)", i, p.verbs[i], v, p.verbs[:], loc = loc) { return } } if !testing.expectf(t, len(p.points) == len(pts), "%d points, wanted %d: %v", len(p.points), len(pts), p.points[:], loc = loc) { return } for want, i in pts { got := p.points[i] ok := abs(got.x - want.x) < 1e-6 && abs(got.y - want.y) < 1e-6 testing.expectf(t, ok, "point %d is %v, wanted %v", i, got, want, loc = loc) } } // ttf_square_font is the font most tests start from: two glyphs, the blank // .notdef and the square, with a short loca. @(private="file") ttf_square_font :: proc(f: ^Tt_Fixture) -> ^TrueType_Font { data := ttf_font(f, []Tt_Table{ {"head", ttf_head(f, 1000, false)}, {"maxp", ttf_maxp(f, 2)}, {"loca", ttf_loca_short(f, 0, 0, 11)}, // glyph 0 empty, glyph 1 at 0..22 {"glyf", GLYF_SQUARE[:]}, }) font, err := tt_parse(data, f.alloc) if err != .None { return nil } return font } // ttf_glyf concatenates glyph programs into a `glyf` table, padding each to a // four-byte boundary, and returns the loca offsets that go with it. @(private="file") ttf_glyf :: proc(f: ^Tt_Fixture, parts: ..[]byte) -> (data: []byte, offs: []u32) { total := 0 for p in parts { total += (len(p) + 3) & ~int(3) } data = make([]byte, total, f.alloc) offs = make([]u32, len(parts) + 1, f.alloc) pos := 0 for p, i in parts { offs[i] = u32(pos) copy(data[pos:], p) pos += (len(p) + 3) & ~int(3) } offs[len(parts)] = u32(pos) return } // ttf_glyph_font builds a font whose only tables are head, maxp, loca and // glyf, with one glyph per part and a long loca. @(private="file") ttf_glyph_font :: proc(f: ^Tt_Fixture, parts: ..[]byte) -> ^TrueType_Font { glyf, offs := ttf_glyf(f, ..parts) data := ttf_font(f, []Tt_Table{ {"head", ttf_head(f, 1000, true)}, {"maxp", ttf_maxp(f, u16(len(parts)))}, {"loca", ttf_loca_long(f, ..offs)}, {"glyf", glyf}, }) font, err := tt_parse(data, f.alloc) if err != .None { return nil } return font } // --------------------------------------------------------------------------- // Simple glyphs // --------------------------------------------------------------------------- @(test) test_tt_simple_glyph :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_square_font(&f) if !testing.expect(t, font != nil, "parse failed") { return } testing.expect_value(t, tt_glyph_count(font), 2) testing.expect_value(t, tt_units_per_em(font), 1000) testing.expect(t, tt_has_glyf(font), "font has glyf outlines") p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) // The last point of the contour is the one the Close verb returns to, so // it is not emitted twice. ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Close}, {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) // Glyph 0 is a legitimately empty entry: no outline, and not an error. path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 0, &p), Error.None) testing.expect(t, path_is_empty(&p), "empty loca entry draws nothing") // Past numGlyphs there is no glyph at all, which is a different answer. path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.No_Glyph) } @(test) test_tt_implied_midpoint :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_glyph_font(&f, nil, GLYF_QUAD[:]) if !testing.expect(t, font != nil, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) // (0,0) on, (50,100) off, (150,100) off, (100,0) on. The two off-curve // points imply an on-curve point at (100,100) between them, so the contour // is two quadratics, each written as the cubic with controls two thirds of // the way to the quadratic's control point. K :: 2.0 / 3.0 ttf_expect_path(t, &p, {.Move, .Curve, .Curve, .Close}, { {0, 0}, {50 * K, 100 * K}, {100 - 50 * K, 100}, {100, 100}, {100 + 50 * K, 100}, {100 + 50 * K, 100 * K}, {100, 0}, }) } @(test) test_tt_all_off_curve :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_glyph_font(&f, nil, GLYF_CIRCLE[:]) if !testing.expect(t, font != nil, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) // No point is on the curve, so the contour starts at the midpoint of the // last and first control points -- (0,100) and (0,0) -- and runs as four // quadratics from midpoint to midpoint, ending back where it started. K :: 2.0 / 3.0 M :: 50 * K ttf_expect_path(t, &p, {.Move, .Curve, .Curve, .Curve, .Curve, .Close}, { {0, 50}, {0, 50 - M}, {50 - M, 0}, {50, 0}, {50 + M, 0}, {100, 50 - M}, {100, 50}, {100, 50 + M}, {50 + M, 100}, {50, 100}, {50 - M, 100}, {0, 50 + M}, {0, 50}, }) } @(test) test_tt_backwards_contour_ends :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_glyph_font(&f, nil, GLYF_CONTOURS[:]) if !testing.expect(t, font != nil, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) // The first end index claims all six points. The two after it are dropped // rather than allowed to carve a second contour out of the same points. ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Line, .Line, .Close}, {{10, 10}, {20, 20}, {30, 30}, {40, 40}, {50, 50}, {60, 60}}) } @(test) test_tt_repeat_flag :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_glyph_font(&f, nil, GLYF_REPEAT[:]) if !testing.expect(t, font != nil, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Close}, {{10, 10}, {30, 10}, {30, 30}, {60, 60}}) } // --------------------------------------------------------------------------- // loca // --------------------------------------------------------------------------- @(test) test_tt_loca_short_and_long :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // The same glyph reached through a short loca, whose entries are halved, // and a long one, whose entries are not. short_font := ttf_square_font(&f) long_data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"maxp", ttf_maxp(&f, 2)}, {"loca", ttf_loca_long(&f, 0, 0, 22)}, {"glyf", GLYF_SQUARE[:]}, }) long_font, err := tt_parse(long_data, f.alloc) if !testing.expect(t, err == .None && short_font != nil, "parse failed") { return } testing.expect(t, !short_font.long_loca && long_font.long_loca, "loca formats differ") a := path_make() defer path_destroy(&a) b := path_make() defer path_destroy(&b) testing.expect_value(t, tt_glyph_path(short_font, 1, &a), Error.None) testing.expect_value(t, tt_glyph_path(long_font, 1, &b), Error.None) ttf_expect_path(t, &a, {.Move, .Line, .Line, .Line, .Close}, {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) ttf_expect_path(t, &b, {.Move, .Line, .Line, .Line, .Close}, {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) } @(test) test_tt_loca_non_monotonic :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // Two copies of the square, with glyph 1 pointed backwards through the // middle of the table: from 24 down to 0. A subsetter that rewrites loca // badly produces exactly this. The backwards entry is an empty glyph, and // the glyph after it still has to work. glyf, _ := ttf_glyf(&f, GLYF_SQUARE[:], GLYF_SQUARE[:]) data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"maxp", ttf_maxp(&f, 3)}, {"loca", ttf_loca_long(&f, 0, 24, 0, 48)}, {"glyf", glyf}, }) font, err := tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) testing.expect(t, path_is_empty(&p), "a backwards loca pair is an empty glyph") path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.None) ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Close}, {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) } @(test) test_tt_glyph_runs_past_table :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // loca claims the glyph is a kilobyte long in a 22-byte glyf: the entry is // clamped to the table, and the glyph inside it is complete, so it draws. data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"maxp", ttf_maxp(&f, 3)}, {"loca", ttf_loca_long(&f, 0, 0, 1024, 4096)}, {"glyf", GLYF_SQUARE[:]}, }) font, err := tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Close}, {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) // Glyph 2 starts past the end of the table entirely. path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.None) testing.expect(t, path_is_empty(&p), "a glyph beyond glyf draws nothing") } @(test) test_tt_truncated_glyph :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // Every prefix of a valid glyph: the header, the contour ends, the flags // and both coordinate arrays each run out somewhere. The three shapes end // on a different kind of field -- a short y delta, a padding byte and the // second half of a 16-bit x delta -- so no one bounds check stands in for // the others. cases := [?]struct { glyph: []byte, complete: int, // bytes before the padding, if any }{ {GLYF_SQUARE[:], 22}, {GLYF_QUAD[:], 23}, {GLYF_LINE[:], 20}, } for c in cases { for cut in 1 ..= len(c.glyph) { data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"maxp", ttf_maxp(&f, 2)}, {"loca", ttf_loca_long(&f, 0, 0, u32(cut))}, {"glyf", c.glyph[:cut]}, }) font, err := tt_parse(data, f.alloc) if !testing.expectf(t, err == .None, "cut %d: parse failed", cut) { continue } p := path_make() defer path_destroy(&p) gerr := tt_glyph_path(font, 1, &p) if cut >= c.complete { testing.expectf(t, gerr == .None, "cut %d: got %v", cut, gerr) } else { testing.expectf(t, gerr == .Bad_TrueType, "cut %d: got %v", cut, gerr) testing.expectf(t, path_is_empty(&p), "cut %d: emitted %v", cut, p.verbs[:]) } } } } // --------------------------------------------------------------------------- // Composite glyphs // --------------------------------------------------------------------------- // A component with a word-sized x/y offset and nothing else. @(private="file") GLYF_COMP_OFFSET := [?]u8{ 0xFF, 0xFF, // numberOfContours = -1 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // bbox 0x00, 0x03, // ARG_1_AND_2_ARE_WORDS | ARGS_ARE_XY_VALUES 0x00, 0x01, // component glyph 1 0x00, 0xC8, 0x00, 0x32, // dx = 200, dy = 50 } // The same, with WE_HAVE_A_SCALE. The offset is not scaled, which is the // Microsoft behaviour and the default. @(private="file") GLYF_COMP_SCALE := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, // WORDS | XY | WE_HAVE_A_SCALE 0x00, 0x01, 0x00, 0x0A, 0x00, 0x14, // dx = 10, dy = 20 0x20, 0x00, // scale = 0.5 in F2Dot14 } // SCALED_COMPONENT_OFFSET puts the offset in the component's space: 101 halves // to 50.5. Without ROUND_XY_TO_GRID that half unit survives. @(private="file") GLYF_COMP_SCALED_OFFSET := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0B, // WORDS | XY | SCALE | SCALED_OFFSET 0x00, 0x01, 0x00, 0x65, 0x00, 0x00, // dx = 101, dy = 0 0x20, 0x00, } // The same again with ROUND_XY_TO_GRID, which rounds 50.5 to 51. @(private="file") GLYF_COMP_SCALED_ROUND := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0F, // ... | ROUND_XY_TO_GRID 0x00, 0x01, 0x00, 0x65, 0x00, 0x00, 0x20, 0x00, } // A TWO_BY_TWO transform: the matrix {0, 1, -1, 0} rotates a quarter turn. @(private="file") GLYF_COMP_2X2 := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, // WORDS | XY | WE_HAVE_A_TWO_BY_TWO 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x00, 0x00, // a=0 b=1 c=-1 d=0 } // Two components: the second is placed by matching its point 0 onto point 2 of // the composite so far, rather than by an offset. @(private="file") GLYF_COMP_MATCH := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, // WORDS | XY | MORE_COMPONENTS 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // byte args, point matching, last 0x00, 0x01, 0x02, 0x00, // composite point 2, component point 0 } // A composite that is its own component: glyph 2 of the font it is put in. @(private="file") GLYF_COMP_SELF := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, } // USE_MY_METRICS: the composite's advance comes from component glyph 1. @(private="file") GLYF_COMP_METRICS := [?]u8{ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, // WORDS | XY | USE_MY_METRICS 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, } @(private="file") SQUARE_VERBS := [?]Path_Verb{.Move, .Line, .Line, .Line, .Close} @(private="file") ttf_composite_path :: proc(t: ^testing.T, f: ^Tt_Fixture, comp: []byte, p: ^Path) -> bool { font := ttf_glyph_font(f, nil, GLYF_SQUARE[:], comp) if !testing.expect(t, font != nil, "parse failed") { return false } err := tt_glyph_path(font, 2, p) return testing.expectf(t, err == .None, "glyph 2: %v", err) } @(test) test_tt_composite_offset :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) p := path_make() defer path_destroy(&p) if !ttf_composite_path(t, &f, GLYF_COMP_OFFSET[:], &p) { return } ttf_expect_path(t, &p, SQUARE_VERBS[:], {{200, 50}, {300, 50}, {300, 150}, {200, 150}}) } @(test) test_tt_composite_scale :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) p := path_make() defer path_destroy(&p) if !ttf_composite_path(t, &f, GLYF_COMP_SCALE[:], &p) { return } ttf_expect_path(t, &p, SQUARE_VERBS[:], {{10, 20}, {60, 20}, {60, 70}, {10, 70}}) } @(test) test_tt_composite_scaled_offset_and_rounding :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) p := path_make() defer path_destroy(&p) if !ttf_composite_path(t, &f, GLYF_COMP_SCALED_OFFSET[:], &p) { return } ttf_expect_path(t, &p, SQUARE_VERBS[:], {{50.5, 0}, {100.5, 0}, {100.5, 50}, {50.5, 50}}) path_clear(&p) if !ttf_composite_path(t, &f, GLYF_COMP_SCALED_ROUND[:], &p) { return } ttf_expect_path(t, &p, SQUARE_VERBS[:], {{51, 0}, {101, 0}, {101, 50}, {51, 50}}) } @(test) test_tt_composite_two_by_two :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) p := path_make() defer path_destroy(&p) if !ttf_composite_path(t, &f, GLYF_COMP_2X2[:], &p) { return } // (x,y) -> (a*x + c*y, b*x + d*y) = (-y, x): a quarter turn. ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {0, 100}, {-100, 100}, {-100, 0}}) } @(test) test_tt_composite_point_match :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) p := path_make() defer path_destroy(&p) if !ttf_composite_path(t, &f, GLYF_COMP_MATCH[:], &p) { return } // The first copy sits at the origin. The second moves so that its point 0, // at (0,0), lands on the composite's point 2, at (100,100). verbs := [?]Path_Verb{ .Move, .Line, .Line, .Line, .Close, .Move, .Line, .Line, .Line, .Close, } ttf_expect_path(t, &p, verbs[:], { {0, 0}, {100, 0}, {100, 100}, {0, 100}, {100, 100}, {200, 100}, {200, 200}, {100, 200}, }) } @(test) test_tt_composite_self_reference :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_glyph_font(&f, nil, GLYF_SQUARE[:], GLYF_COMP_SELF[:]) if !testing.expect(t, font != nil, "parse failed") { return } p := path_make() defer path_destroy(&p) start := time.now() err := tt_glyph_path(font, 2, &p) elapsed := time.since(start) testing.expect_value(t, err, Error.Recursion_Limit) testing.expect(t, path_is_empty(&p), "a glyph that is its own component draws nothing") testing.expectf(t, elapsed < time.Second, "took %v", elapsed) } // --------------------------------------------------------------------------- // hmtx // --------------------------------------------------------------------------- @(test) test_tt_hmtx_trailing_run :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // Two long metrics for four glyphs: everything from glyph 1 on shares the // last advance, which is how a font with a long tail of equal-width glyphs // stays small. ISO 14496-22, 5.2.4. hmtx := ttf_bytes(&f, 0x01, 0xF4, 0x00, 0x00, // glyph 0: advance 500 0x02, 0x58, 0x00, 0x00, // glyph 1: advance 600 0x00, 0x0A, // glyph 2: left side bearing only 0x00, 0x14) // glyph 3: left side bearing only data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 2048, false)}, {"maxp", ttf_maxp(&f, 4)}, {"hhea", ttf_hhea(&f, 2)}, {"hmtx", hmtx}, }) font, err := tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } testing.expect_value(t, tt_units_per_em(font), 2048) testing.expect_value(t, tt_glyph_advance(font, 0), 500) testing.expect_value(t, tt_glyph_advance(font, 1), 600) testing.expect_value(t, tt_glyph_advance(font, 2), 600) testing.expect_value(t, tt_glyph_advance(font, 3), 600) // Past the end of the font the answer is still the last advance rather // than a read off the end of the table. testing.expect_value(t, tt_glyph_advance(font, 400), 600) } @(test) test_tt_hmtx_without_hhea :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // No hhea at all, so nothing says how many long metrics hmtx holds. The // first entry stands in for every glyph rather than every glyph coming // back zero wide and piling up on one spot. data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, false)}, {"maxp", ttf_maxp(&f, 4)}, {"hmtx", ttf_bytes(&f, 0x01, 0xF4, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00)}, }) font, err := tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } testing.expect_value(t, tt_glyph_advance(font, 0), 500) testing.expect_value(t, tt_glyph_advance(font, 3), 500) // With no hmtx either there is nothing to report. data = ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, false)}, {"maxp", ttf_maxp(&f, 4)}, }) font, err = tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } testing.expect_value(t, tt_glyph_advance(font, 0), 0) } @(test) test_tt_use_my_metrics :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) check :: proc(t: ^testing.T, f: ^Tt_Fixture, comp: []byte) -> f64 { glyf, offs := ttf_glyf(f, nil, GLYF_SQUARE[:], comp) hmtx := ttf_bytes(f, 0x01, 0xF4, 0x00, 0x00, // glyph 0: 500 0x02, 0x58, 0x00, 0x00, // glyph 1: 600 0x03, 0xE7, 0x00, 0x00) // glyph 2: 999 data := ttf_font(f, []Tt_Table{ {"head", ttf_head(f, 1000, true)}, {"maxp", ttf_maxp(f, 3)}, {"hhea", ttf_hhea(f, 3)}, {"hmtx", hmtx}, {"loca", ttf_loca_long(f, ..offs)}, {"glyf", glyf}, }) font, err := tt_parse(data, f.alloc) if err != .None || font == nil { return -1 } return tt_glyph_advance(font, 2) } // The composite's own hmtx entry says 999; USE_MY_METRICS makes it take // component glyph 1's 600 instead. testing.expect_value(t, check(t, &f, GLYF_COMP_METRICS[:]), 600) // Without the flag the composite keeps its own. testing.expect_value(t, check(t, &f, GLYF_COMP_OFFSET[:]), 999) } // --------------------------------------------------------------------------- // cmap // --------------------------------------------------------------------------- // Three segments. The first maps 'A'..'C' through idDelta; the second maps // 0x100..0x102 through the glyphIdArray indirection, with a zero in the middle // standing for a code the font does not have; the third is the 0xFFFF // terminator every format 4 table must end with. searchRange, entrySelector // and rangeShift are deliberately zero: they are redundant with segCountX2 and // wrong often enough that this reader recomputes the search instead. @(private="file") CMAP4 := [?]u8{ 0x00, 0x04, // format 0x00, 0x2E, // length = 46 0x00, 0x00, // language 0x00, 0x06, // segCountX2 = 6 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x01, 0x02, 0xFF, 0xFF, // endCode 0x00, 0x00, // reservedPad 0x00, 0x41, 0x01, 0x00, 0xFF, 0xFF, // startCode 0xFF, 0xC0, 0x00, 0x01, 0x00, 0x01, // idDelta 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // idRangeOffset 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0C, // glyphIdArray = 10, missing, 12 } // 'A'..'C' and two astral code points, in the 32-bit grouped format. @(private="file") CMAP12 := [?]u8{ 0x00, 0x0C, 0x00, 0x00, // format 12, reserved 0x00, 0x00, 0x00, 0x28, // length = 40 0x00, 0x00, 0x00, 0x00, // language 0x00, 0x00, 0x00, 0x02, // nGroups = 2 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0xF6, 0x00, 0x00, 0x01, 0xF6, 0x01, 0x00, 0x00, 0x00, 0x64, } // A trimmed table covering three codes. @(private="file") CMAP6 := [?]u8{ 0x00, 0x06, // format 0x00, 0x10, // length = 16 0x00, 0x00, // language 0x00, 0x41, // firstCode = 'A' 0x00, 0x03, // entryCount 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, } // A (3,0) symbol table: one glyph at the bare byte 0x42 and one in the // private-use area at 0xF041, so that both halves of the 0xF000 convention are // exercised by one table. @(private="file") CMAP4_SYMBOL := [?]u8{ 0x00, 0x04, 0x00, 0x28, // length = 40 0x00, 0x00, 0x00, 0x06, // segCountX2 = 6 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xF0, 0x41, 0xFF, 0xFF, // endCode 0x00, 0x00, 0x00, 0x42, 0xF0, 0x41, 0xFF, 0xFF, // startCode 0xFF, 0xC2, 0x0F, 0xC2, 0x00, 0x01, // idDelta: 0x42->4, 0xF041->3 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // idRangeOffset } // A format 2 stub: the high-byte scheme for legacy CJK encodings, which this // reader does not implement and must therefore not choose. @(private="file") CMAP2_STUB := [?]u8{ 0x00, 0x02, 0x02, 0x0C, 0x00, 0x00, } // ttf_cmap0 builds a 262-byte byte-encoding table with the given code/glyph // pairs set. @(private="file") ttf_cmap0 :: proc(f: ^Tt_Fixture, pairs: ..[2]u8) -> []byte { out := make([]byte, 262, f.alloc) out[1] = 0 // format 0 out[2] = 0x01 // length 262 out[3] = 0x06 for pr in pairs { out[6 + int(pr[0])] = pr[1] } return out } @(private="file") ttf_cmap_font :: proc(f: ^Tt_Fixture, recs: ..Tt_Cmap_Rec) -> ^TrueType_Font { data := ttf_font(f, []Tt_Table{ {"head", ttf_head(f, 1000, false)}, {"maxp", ttf_maxp(f, 200)}, {"cmap", ttf_cmap(f, ..recs)}, }) font, err := tt_parse(data, f.alloc) if err != .None { return nil } return font } @(private="file") ttf_expect_gid :: proc(t: ^testing.T, got: u16, ok: bool, want: u16, loc := #caller_location) { testing.expectf(t, ok == (want != 0), "found=%v, wanted %v", ok, want != 0, loc = loc) testing.expectf(t, got == want, "gid %d, wanted %d", got, want, loc = loc) } @(test) test_tt_cmap_format4 :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_cmap_font(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]}) if !testing.expect(t, font != nil, "parse failed") { return } // The first segment: gid = code + idDelta, modulo 65536. g, ok := tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 1) g, ok = tt_gid_for_unicode(font, 'B'); ttf_expect_gid(t, g, ok, 2) g, ok = tt_gid_for_unicode(font, 'C'); ttf_expect_gid(t, g, ok, 3) // Below the first segment's startCode, and between two segments: the // search lands on a segment that does not contain the code. g, ok = tt_gid_for_unicode(font, 0x40); ttf_expect_gid(t, g, ok, 0) g, ok = tt_gid_for_unicode(font, 'D'); ttf_expect_gid(t, g, ok, 0) // The second segment: gid comes from glyphIdArray through idRangeOffset, // with idDelta added to what it finds there. g, ok = tt_gid_for_unicode(font, 0x100); ttf_expect_gid(t, g, ok, 11) g, ok = tt_gid_for_unicode(font, 0x102); ttf_expect_gid(t, g, ok, 13) // A zero in glyphIdArray means no glyph, and idDelta is not added to it. g, ok = tt_gid_for_unicode(font, 0x101); ttf_expect_gid(t, g, ok, 0) // The terminating segment maps 0xFFFF to 0. g, ok = tt_gid_for_unicode(font, 0xFFFF); ttf_expect_gid(t, g, ok, 0) // Beyond the BMP a format 4 table has nothing at all. g, ok = tt_gid_for_unicode(font, 0x1F600); ttf_expect_gid(t, g, ok, 0) } @(test) test_tt_cmap_format12 :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_cmap_font(&f, Tt_Cmap_Rec{3, 10, CMAP12[:]}) if !testing.expect(t, font != nil, "parse failed") { return } g, ok := tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 5) g, ok = tt_gid_for_unicode(font, 'C'); ttf_expect_gid(t, g, ok, 7) g, ok = tt_gid_for_unicode(font, 0x40); ttf_expect_gid(t, g, ok, 0) g, ok = tt_gid_for_unicode(font, 0x44); ttf_expect_gid(t, g, ok, 0) // The second group is past the BMP, which is the whole point of format 12. g, ok = tt_gid_for_unicode(font, 0x1F600); ttf_expect_gid(t, g, ok, 100) g, ok = tt_gid_for_unicode(font, 0x1F601); ttf_expect_gid(t, g, ok, 101) g, ok = tt_gid_for_unicode(font, 0x1F602); ttf_expect_gid(t, g, ok, 0) } @(test) test_tt_cmap_mac_and_symbol :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_cmap_font(&f, Tt_Cmap_Rec{1, 0, ttf_cmap0(&f, {0x41, 5}, {0xA5, 6})}, Tt_Cmap_Rec{3, 0, CMAP4_SYMBOL[:]}) if !testing.expect(t, font != nil, "parse failed") { return } // (1,0) is a Mac Roman byte table and is reached only through the mac // entry point: it is not a Unicode cmap and must not be used as one. g, ok := tt_gid_for_mac_code(font, 0x41); ttf_expect_gid(t, g, ok, 5) g, ok = tt_gid_for_mac_code(font, 0xA5); ttf_expect_gid(t, g, ok, 6) g, ok = tt_gid_for_mac_code(font, 0x42); ttf_expect_gid(t, g, ok, 0) g, ok = tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 0) // (3,0) is a symbol table: 0x42 is mapped by the bare byte and 0x41 only // through the 0xF000 convention. Both have to be tried. g, ok = tt_gid_for_symbol(font, 0x42); ttf_expect_gid(t, g, ok, 4) g, ok = tt_gid_for_symbol(font, 0x41); ttf_expect_gid(t, g, ok, 3) g, ok = tt_gid_for_symbol(font, 0x99); ttf_expect_gid(t, g, ok, 0) // The same font read as Unicode has nothing: a (3,0) table is not one. g, ok = tt_gid_for_unicode(font, 0x42); ttf_expect_gid(t, g, ok, 0) } @(test) test_tt_cmap_format6 :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // A second subtable follows this one, so the bytes after its entryCount // belong to another table: a code past the end of the trimmed range has to // be rejected by the count, not by running out of buffer. font := ttf_cmap_font(&f, Tt_Cmap_Rec{1, 0, CMAP6[:]}, Tt_Cmap_Rec{3, 1, CMAP4[:]}) if !testing.expect(t, font != nil, "parse failed") { return } g, ok := tt_gid_for_mac_code(font, 0x41); ttf_expect_gid(t, g, ok, 7) g, ok = tt_gid_for_mac_code(font, 0x43); ttf_expect_gid(t, g, ok, 9) g, ok = tt_gid_for_mac_code(font, 0x40); ttf_expect_gid(t, g, ok, 0) g, ok = tt_gid_for_mac_code(font, 0x44); ttf_expect_gid(t, g, ok, 0) } @(test) test_tt_cmap_preference :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // (3,1) and (3,10) both map 'A', to different glyphs. The full-repertoire // table wins whichever order the records are in. font := ttf_cmap_font(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]}, Tt_Cmap_Rec{3, 10, CMAP12[:]}) if !testing.expect(t, font != nil, "parse failed") { return } g, ok := tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 5) font = ttf_cmap_font(&f, Tt_Cmap_Rec{3, 10, CMAP12[:]}, Tt_Cmap_Rec{3, 1, CMAP4[:]}) if !testing.expect(t, font != nil, "parse failed") { return } g, ok = tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 5) // A (3,1) in a format this reader cannot read must not be chosen over a // platform-0 table it can. font = ttf_cmap_font(&f, Tt_Cmap_Rec{3, 1, CMAP2_STUB[:]}, Tt_Cmap_Rec{0, 3, CMAP4[:]}) if !testing.expect(t, font != nil, "parse failed") { return } g, ok = tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 1) // A record whose offset is past the end of the table is skipped rather // than followed. bad := ttf_cmap(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]}) bad[10] = 0xFF // the record's offset, high byte data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, false)}, {"maxp", ttf_maxp(&f, 200)}, {"cmap", bad}, }) broken, err := tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } g, ok = tt_gid_for_unicode(broken, 'A'); ttf_expect_gid(t, g, ok, 0) } @(test) test_tt_no_cmap :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // A font with no cmap and no post at all parses: those tables are how a // glyph is found by character, and a CIDFontType2 subset needs neither. font := ttf_square_font(&f) if !testing.expect(t, font != nil, "parse failed") { return } _, ok := tt_gid_for_unicode(font, 'A') testing.expect(t, !ok, "no unicode cmap") _, ok = tt_gid_for_mac_code(font, 0x41) testing.expect(t, !ok, "no mac cmap") _, ok = tt_gid_for_symbol(font, 0x41) testing.expect(t, !ok, "no symbol cmap") _, ok = tt_gid_for_name(font, "A") testing.expect(t, !ok, "no post table") } // --------------------------------------------------------------------------- // post // --------------------------------------------------------------------------- // ttf_post prefixes the 32-byte header -- a version and nine fields nothing // here reads -- to a literal tail. @(private="file") ttf_post :: proc(f: ^Tt_Fixture, version: u32, tail: []u8) -> []byte { out := make([]byte, 32 + len(tail), f.alloc) out[0] = u8(version >> 24) out[1] = u8(version >> 16) out[2] = u8(version >> 8) out[3] = u8(version) copy(out[32:], tail) return out } // Three glyphs: one named by a standard Macintosh index, one by a Pascal // string of its own, and one by a standard index that is not zero. @(private="file") POST2_TAIL := [?]u8{ 0x00, 0x03, // numberOfGlyphs 0x00, 0x00, 0x01, 0x02, 0x00, 0x03, // indices: 0, 258, 3 0x06, 'A', 'a', 'c', 'u', 't', 'e', // the one custom name } @(private="file") ttf_post_font :: proc(f: ^Tt_Fixture, post: []byte) -> ^TrueType_Font { data := ttf_font(f, []Tt_Table{ {"head", ttf_head(f, 1000, false)}, {"maxp", ttf_maxp(f, 3)}, {"post", post}, }) font, err := tt_parse(data, f.alloc) if err != .None { return nil } return font } @(test) test_tt_post_names :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) font := ttf_post_font(&f, ttf_post(&f, 0x00020000, POST2_TAIL[:])) if !testing.expect(t, font != nil, "parse failed") { return } g, ok := tt_gid_for_name(font, ".notdef") testing.expect(t, ok && g == 0, "index below 258 is a standard Macintosh name") g, ok = tt_gid_for_name(font, "Aacute") testing.expect(t, ok && g == 1, "index 258 is the first Pascal string") g, ok = tt_gid_for_name(font, "space") testing.expect(t, ok && g == 2, "index 3 is the fourth standard name") _, ok = tt_gid_for_name(font, "Q") testing.expect(t, !ok, "a name the font does not have") _, ok = tt_gid_for_name(font, "") testing.expect(t, !ok, "the empty name never matches") // Format 1.0 says the font is the 258 standard glyphs in order. font = ttf_post_font(&f, ttf_post(&f, 0x00010000, nil)) if !testing.expect(t, font != nil, "parse failed") { return } g, ok = tt_gid_for_name(font, "nonmarkingreturn") testing.expect(t, ok && g == 2, "format 1.0 is the standard order") // Format 3.0 -- what a subsetter writes -- has no names, which is not an // error and must not stop the font loading. font = ttf_post_font(&f, ttf_post(&f, 0x00030000, nil)) if !testing.expect(t, font != nil, "parse failed") { return } _, ok = tt_gid_for_name(font, "space") testing.expect(t, !ok, "format 3.0 carries no names") // A post cut off inside its Pascal strings still names the glyphs whose // names arrived. truncated := ttf_post(&f, 0x00020000, POST2_TAIL[:len(POST2_TAIL) - 3]) font = ttf_post_font(&f, truncated) if !testing.expect(t, font != nil, "parse failed") { return } g, ok = tt_gid_for_name(font, "space") testing.expect(t, ok && g == 2, "the standard names survive a cut") _, ok = tt_gid_for_name(font, "Aacute") testing.expect(t, !ok, "the cut-off Pascal string is dropped") } // --------------------------------------------------------------------------- // The table directory // --------------------------------------------------------------------------- @(test) test_tt_otto_has_no_glyf :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) cff := ttf_bytes(&f, 0x01, 0x00, 0x04, 0x02, 0xDE, 0xAD) data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, false)}, {"maxp", ttf_maxp(&f, 4)}, {"CFF ", cff}, }, 0x4F54544F) font, err := tt_parse(data, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } testing.expect(t, font.is_otto, "OTTO was recognised") testing.expect(t, !tt_has_glyf(font), "CFF outlines, not glyf") got := tt_cff_data(font) testing.expect_value(t, len(got), len(cff)) testing.expect(t, len(got) == len(cff) && got[4] == 0xDE, "the CFF table came back") // Asking for an outline from a font that has none is not a crash. p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.No_Glyph) } // ttf_collection wraps one font in a TrueType Collection header, moving its // tables and fixing up the offsets in its directory. @(private="file") ttf_collection :: proc(f: ^Tt_Fixture, font: []byte) -> []byte { HDR :: 16 out := make([]byte, HDR + len(font), f.alloc) copy(out, []byte{'t', 't', 'c', 'f', 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, HDR}) copy(out[HDR:], font) n := int(u16(font[4]) << 8 | u16(font[5])) for i in 0 ..< n { rec := HDR + 12 + 16 * i off := u32(out[rec + 8]) << 24 | u32(out[rec + 9]) << 16 | u32(out[rec + 10]) << 8 | u32(out[rec + 11]) off += HDR out[rec + 8] = u8(off >> 24) out[rec + 9] = u8(off >> 16) out[rec + 10] = u8(off >> 8) out[rec + 11] = u8(off) } return out } @(test) test_tt_collection :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) inner := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, false)}, {"maxp", ttf_maxp(&f, 2)}, {"loca", ttf_loca_short(&f, 0, 0, 11)}, {"glyf", GLYF_SQUARE[:]}, }) font, err := tt_parse(ttf_collection(&f, inner), f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) } @(test) test_tt_directory_tolerances :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) // Not a font at all. _, err := tt_parse(nil, f.alloc) testing.expect_value(t, err, Error.Bad_TrueType) _, err = tt_parse(ttf_bytes(&f, 'O', 'T', 'T', 'O'), f.alloc) testing.expect_value(t, err, Error.Bad_TrueType) junk := ttf_font(&f, []Tt_Table{{"head", ttf_head(&f, 1000, false)}}, 0x25504446) _, err = tt_parse(junk, f.alloc) testing.expect_value(t, err, Error.Bad_TrueType) // unitsPerEm of zero would scale every glyph to infinity. data := ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 0, false)}, {"maxp", ttf_maxp(&f, 2)}, }) font, perr := tt_parse(data, f.alloc) if !testing.expect(t, perr == .None, "parse failed") { return } testing.expect_value(t, tt_units_per_em(font), 1000) // No maxp: the glyph count comes from loca, which has one entry more than // there are glyphs. data = ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"loca", ttf_loca_long(&f, 0, 0, 22)}, {"glyf", GLYF_SQUARE[:]}, }) font, perr = tt_parse(data, f.alloc) if !testing.expect(t, perr == .None, "parse failed") { return } testing.expect_value(t, tt_glyph_count(font), 2) p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) // maxp says two glyphs while loca describes three. The count in maxp is // what a cmap's glyph ids are validated against, so it wins: the third // entry is not reachable however complete it looks. two_squares, offs := ttf_glyf(&f, nil, GLYF_SQUARE[:], GLYF_SQUARE[:]) data = ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"maxp", ttf_maxp(&f, 2)}, {"loca", ttf_loca_long(&f, ..offs)}, {"glyf", two_squares}, }) font, perr = tt_parse(data, f.alloc) if !testing.expect(t, perr == .None, "parse failed") { return } testing.expect_value(t, tt_glyph_count(font), 2) path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) testing.expect(t, !path_is_empty(&p), "glyph 1 is inside the count") path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.No_Glyph) // A record whose length runs past the end of the file is clamped to it, // and one whose offset does is dropped. Both are ordinary damage in an // embedded font program. data = ttf_font(&f, []Tt_Table{ {"head", ttf_head(&f, 1000, true)}, {"maxp", ttf_maxp(&f, 2)}, {"loca", ttf_loca_long(&f, 0, 0, 22)}, {"glyf", GLYF_SQUARE[:]}, {"cmap", ttf_cmap(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]})}, }) glyf_rec := 12 + 16 * 3 data[glyf_rec + 12] = 0xFF // glyf length, far past the end cmap_rec := 12 + 16 * 4 data[cmap_rec + 8] = 0x7F // cmap offset, past the end font, perr = tt_parse(data, f.alloc) if !testing.expect(t, perr == .None, "parse failed") { return } path_clear(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) _, ok := tt_gid_for_unicode(font, 'A') testing.expect(t, !ok, "a table past the end of the file is not there") } // --------------------------------------------------------------------------- // Damage // --------------------------------------------------------------------------- // ttf_rich_font is a font with one of everything: two real glyphs, a // composite, all three kinds of cmap subtable, metrics and names. It is the // input the damage tests chew on. @(private="file") ttf_rich_font :: proc(f: ^Tt_Fixture) -> []byte { glyf, offs := ttf_glyf(f, nil, GLYF_SQUARE[:], GLYF_QUAD[:], GLYF_COMP_MATCH[:]) return ttf_font(f, []Tt_Table{ {"head", ttf_head(f, 2048, true)}, {"maxp", ttf_maxp(f, 4)}, {"hhea", ttf_hhea(f, 2)}, {"hmtx", ttf_bytes(f, 0x01, 0xF4, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x14)}, {"loca", ttf_loca_long(f, ..offs)}, {"glyf", glyf}, {"cmap", ttf_cmap(f, Tt_Cmap_Rec{3, 1, CMAP4[:]}, Tt_Cmap_Rec{3, 0, CMAP4_SYMBOL[:]}, Tt_Cmap_Rec{1, 0, CMAP6[:]})}, {"post", ttf_post(f, 0x00020000, POST2_TAIL[:])}, }) } // ttf_exercise calls every entry point over a spread of glyph ids and codes. // It asserts nothing: what it is for is to be run against damaged input under // the bounds checker, where a read off the end of a table is a crash. @(private="file") ttf_exercise :: proc(font: ^TrueType_Font) { if font == nil { return } _ = tt_glyph_count(font) _ = tt_units_per_em(font) _ = tt_has_glyf(font) _ = tt_cff_data(font) p := path_make() defer path_destroy(&p) for gid in u16(0) ..= 6 { path_clear(&p) _ = tt_glyph_path(font, gid, &p) _ = tt_glyph_advance(font, gid) } _ = tt_glyph_path(font, 65535, &p) _ = tt_glyph_advance(font, 65535) for c in rune(0x40) ..= 0x44 { _, _ = tt_gid_for_unicode(font, c) } _, _ = tt_gid_for_unicode(font, 0x1F600) _, _ = tt_gid_for_unicode(font, 0x10FFFF) for c in u8(0x40) ..= 0x44 { _, _ = tt_gid_for_mac_code(font, c) _, _ = tt_gid_for_symbol(font, c) } _, _ = tt_gid_for_name(font, "space") _, _ = tt_gid_for_name(font, "Aacute") } @(test) test_tt_truncated_font :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) whole := ttf_rich_font(&f) start := time.now() for cut in 0 ..= len(whole) { font, err := tt_parse(whole[:cut], f.alloc) // Whether a prefix parses depends on where it was cut; what matters is // that everything it then answers is bounded by the bytes it has. testing.expectf(t, (font != nil) == (err == .None), "cut %d: %v", cut, err) ttf_exercise(font) } elapsed := time.since(start) testing.expectf(t, elapsed < 30 * time.Second, "took %v", elapsed) // The whole font still works, so the sweep above was not measuring a font // that never parsed in the first place. font, err := tt_parse(whole, f.alloc) if !testing.expect(t, err == .None, "parse failed") { return } g, ok := tt_gid_for_unicode(font, 'A') testing.expect(t, ok && g == 1, "the undamaged font maps 'A'") p := path_make() defer path_destroy(&p) testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) testing.expect(t, !path_is_empty(&p), "the undamaged font draws glyph 1") } @(test) test_tt_corrupted_font :: proc(t: ^testing.T) { f: Tt_Fixture ttf_init(&f) defer ttf_destroy(&f) whole := ttf_rich_font(&f) scratch := make([]byte, len(whole), f.alloc) start := time.now() for pattern in ([?]u8{0x00, 0xFF, 0x7F}) { for i in 0 ..< len(whole) { copy(scratch, whole) scratch[i] = pattern font, err := tt_parse(scratch, f.alloc) testing.expectf(t, (font != nil) == (err == .None), "byte %d: %v", i, err) ttf_exercise(font) } } elapsed := time.since(start) testing.expectf(t, elapsed < 30 * time.Second, "took %v", elapsed) }