Files

1516 lines
55 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
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)
}