1516 lines
55 KiB
Odin
1516 lines
55 KiB
Odin
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)
|
|
}
|