Files

2172 lines
86 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:testing"
// Tests for font.odin: the PDF font dictionary, its encodings, its widths and
// its CID machinery.
//
// The Document fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_close` --
// live in function_test.odin.
//
// The font programs here are assembled from byte literals rather than taken
// from real files, because what is under test is the *dictionary's* effect on
// them: which cmap subtable a symbolic flag selects, which name a
// /Differences entry substitutes, which glyph a CID reaches. The programs
// themselves are tested in font_truetype_test.odin and font_cff_test.odin, so
// the ones built here are the smallest that can answer a lookup -- often a
// cmap and a glyph count and nothing else.
//
// Everything is allocated from the Document's arena, so a failing assertion
// cannot leak: `close` reclaims the lot however the test left.
// ---------------------------------------------------------------------------
// Byte assembly
// ---------------------------------------------------------------------------
@(private = "file")
fn_alloc :: proc(doc: ^Document, n: int) -> []byte {
return make([]byte, n, doc.allocator)
}
@(private = "file")
fn_w16 :: proc(b: []byte, off: int, v: u16) {
b[off] = u8(v >> 8)
b[off + 1] = u8(v)
}
@(private = "file")
fn_w32 :: 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)
}
@(private = "file")
Fn_Table :: struct {
tag: string,
data: []byte,
}
// fn_sfnt writes a table directory around `tables`.
@(private = "file")
fn_sfnt :: proc(doc: ^Document, tables: []Fn_Table, sfnt: u32 = 0x00010000) -> []byte {
total := 12 + 16 * len(tables)
for tb in tables {
total += (len(tb.data) + 3) & ~int(3)
}
out := fn_alloc(doc, total)
fn_w32(out, 0, sfnt)
fn_w16(out, 4, u16(len(tables)))
pos := 12 + 16 * len(tables)
for tb, i in tables {
rec := 12 + 16 * i
for k in 0 ..< 4 {
out[rec + k] = tb.tag[k] if k < len(tb.tag) else ' '
}
fn_w32(out, rec + 8, u32(pos))
fn_w32(out, rec + 12, u32(len(tb.data)))
copy(out[pos:], tb.data)
pos += (len(tb.data) + 3) & ~int(3)
}
return out
}
@(private = "file")
fn_head :: proc(doc: ^Document, upem: u16) -> []byte {
out := fn_alloc(doc, 54)
fn_w16(out, 18, upem)
return out // indexToLocFormat 0: short loca
}
@(private = "file")
fn_maxp :: proc(doc: ^Document, num_glyphs: u16) -> []byte {
out := fn_alloc(doc, 32)
fn_w32(out, 0, 0x00010000)
fn_w16(out, 4, num_glyphs)
return out
}
@(private = "file")
fn_hhea :: proc(doc: ^Document, n_metrics: u16) -> []byte {
out := fn_alloc(doc, 36)
fn_w16(out, 34, n_metrics)
return out
}
// fn_hmtx writes one longHorMetric per advance: the advance itself and a zero
// left side bearing.
@(private = "file")
fn_hmtx :: proc(doc: ^Document, advances: []u16) -> []byte {
out := fn_alloc(doc, 4 * len(advances))
for a, i in advances {
fn_w16(out, 4 * i, a)
}
return out
}
@(private = "file")
Fn_Cmap_Rec :: struct {
plat, enc: u16,
sub: []byte,
}
@(private = "file")
fn_cmap :: proc(doc: ^Document, recs: ..Fn_Cmap_Rec) -> []byte {
total := 4 + 8 * len(recs)
for r in recs {
total += len(r.sub)
}
out := fn_alloc(doc, total)
fn_w16(out, 2, u16(len(recs)))
pos := 4 + 8 * len(recs)
for r, i in recs {
rec := 4 + 8 * i
fn_w16(out, rec, r.plat)
fn_w16(out, rec + 2, r.enc)
fn_w32(out, rec + 4, u32(pos))
copy(out[pos:], r.sub)
pos += len(r.sub)
}
return out
}
// Fn_Seg is one format-4 segment: code `lo` maps to glyph `gid`, and the
// codes up to `hi` map to the glyphs after it. The idDelta the table actually
// stores is the difference, which is what the builder computes.
@(private = "file")
Fn_Seg :: struct {
lo, hi, gid: u16,
}
// fn_cmap4 builds a format 4 subtable out of delta segments, appending the
// 0xFFFF terminator the format requires. idRangeOffset is zero throughout, so
// every segment is a pure delta -- the glyphIdArray indirection is
// font_truetype.odin's business and is tested there.
@(private = "file")
fn_cmap4 :: proc(doc: ^Document, segs: ..Fn_Seg) -> []byte {
n := len(segs) + 1
out := fn_alloc(doc, 16 + 8 * n)
fn_w16(out, 0, 4)
fn_w16(out, 2, u16(len(out)))
fn_w16(out, 6, u16(2 * n))
end_p := 14
start_p := end_p + 2 * n + 2
delta_p := start_p + 2 * n
for s, i in segs {
fn_w16(out, end_p + 2 * i, s.hi)
fn_w16(out, start_p + 2 * i, s.lo)
fn_w16(out, delta_p + 2 * i, u16((u32(s.gid) + 0x10000 - u32(s.lo)) & 0xFFFF))
}
fn_w16(out, end_p + 2 * (n - 1), 0xFFFF)
fn_w16(out, start_p + 2 * (n - 1), 0xFFFF)
fn_w16(out, delta_p + 2 * (n - 1), 1)
return out
}
// fn_cmap0 is the 262-byte byte-encoding table.
@(private = "file")
fn_cmap0 :: proc(doc: ^Document, pairs: ..[2]u8) -> []byte {
out := fn_alloc(doc, 262)
fn_w16(out, 2, 262)
for pr in pairs {
out[6 + int(pr[0])] = pr[1]
}
return out
}
// fn_post2 writes a version 2.0 `post` table naming every glyph from its own
// string pool, so none of the names collide with the standard Macintosh set.
@(private = "file")
fn_post2 :: proc(doc: ^Document, names: []string) -> []byte {
pool := 0
for nm in names {
pool += 1 + len(nm)
}
out := fn_alloc(doc, 32 + 2 + 2 * len(names) + pool)
fn_w32(out, 0, 0x00020000)
fn_w16(out, 32, u16(len(names)))
p := 34 + 2 * len(names)
for nm, i in names {
fn_w16(out, 34 + 2 * i, u16(258 + i))
out[p] = u8(len(nm))
copy(out[p + 1:], nm)
p += 1 + len(nm)
}
return out
}
// A blank glyph: zero contours, so `loca` can point at it and `glyf` is
// non-empty, which is what makes tt_has_glyf true.
@(private = "file")
FN_GLYF_BLANK := [?]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
// fn_ttf assembles a TrueType font. `cmaps` may be empty, in which case the
// font has no `cmap` table at all.
@(private = "file")
fn_ttf :: proc(doc: ^Document, num_glyphs: int, upem: u16, cmaps: []Fn_Cmap_Rec,
post: []byte = nil, advances: []u16 = nil) -> []byte {
tables := make([dynamic]Fn_Table, 0, 8, doc.allocator)
append(&tables, Fn_Table{"head", fn_head(doc, upem)})
append(&tables, Fn_Table{"maxp", fn_maxp(doc, u16(num_glyphs))})
// Every glyph is the same blank, so `loca` repeats one offset pair.
loca := fn_alloc(doc, 2 * (num_glyphs + 1))
for i in 0 ..= num_glyphs {
fn_w16(loca, 2 * i, u16(min(i, 1) * (len(FN_GLYF_BLANK) / 2)))
}
append(&tables, Fn_Table{"loca", loca})
append(&tables, Fn_Table{"glyf", FN_GLYF_BLANK[:]})
if len(cmaps) > 0 {
append(&tables, Fn_Table{"cmap", fn_cmap(doc, ..cmaps)})
}
if len(post) > 0 {
append(&tables, Fn_Table{"post", post})
}
if len(advances) > 0 {
append(&tables, Fn_Table{"hhea", fn_hhea(doc, u16(len(advances)))})
append(&tables, Fn_Table{"hmtx", fn_hmtx(doc, advances)})
}
return fn_sfnt(doc, tables[:])
}
// ---------------------------------------------------------------------------
// A minimal CFF
//
// Header, Name INDEX, Top DICT INDEX, an empty String and GSubr INDEX, a
// format 0 charset and a CharStrings INDEX. Every Top DICT offset is written
// in the five-byte integer form so that the dictionary's length does not
// depend on the values in it, which is what lets the layout be computed in one
// pass.
// ---------------------------------------------------------------------------
// FN_CS is one charstring: "400 50 50 rmoveto endchar". rmoveto takes two
// operands, so the third is the glyph's width (T2 3.1) -- 400, since
// nominalWidthX defaults to zero. Six bytes, and it draws a single point.
@(private = "file")
FN_CS := [?]u8{0xF8, 0x24, 0xBD, 0xBD, 0x15, 0x0E}
// FN_CFF_MATRIX is a Top DICT FontMatrix of [0.0005 0 0 0.0005 0 0]: 2000
// units per em rather than the CFF default of 1000. Every operand is written
// in a fixed-width form -- the nibble-packed real of CFF Table 5 for the two
// scales, the five-byte integer for the four zeros -- so that the dictionary's
// length does not depend on its contents.
@(private = "file")
FN_CFF_MATRIX := [?]u8{
0x1E, 0x0A, 0x00, 0x05, 0xFF, // 0.0005
0x1D, 0x00, 0x00, 0x00, 0x00, // 0
0x1D, 0x00, 0x00, 0x00, 0x00, // 0
0x1E, 0x0A, 0x00, 0x05, 0xFF, // 0.0005
0x1D, 0x00, 0x00, 0x00, 0x00, // 0
0x1D, 0x00, 0x00, 0x00, 0x00, // 0
0x0C, 0x07, // FontMatrix
}
@(private = "file")
fn_cff :: proc(doc: ^Document, ids: []u16, is_cid: bool, own_matrix := false) -> []byte {
ng := len(ids) + 1 // .notdef plus one per id
dict_len := 12 + (17 if is_cid else 0) + (len(FN_CFF_MATRIX) if own_matrix else 0)
charset_off := 4 + 6 + (5 + dict_len) + 2 + 2
charset_len := 1 + 2 * len(ids)
cs_off := charset_off + charset_len
cs_len := 3 + (ng + 1) + len(FN_CS) * ng
out := fn_alloc(doc, cs_off + cs_len)
out[0], out[1], out[2], out[3] = 1, 0, 4, 1 // header
// Name INDEX: one name, "T".
p := 4
fn_w16(out, p, 1)
out[p + 2] = 1
out[p + 3] = 1
out[p + 4] = 2
out[p + 5] = 'T'
p += 6
// Top DICT INDEX.
fn_w16(out, p, 1)
out[p + 2] = 1
out[p + 3] = 1
out[p + 4] = u8(1 + dict_len)
d := p + 5
if is_cid {
// ROS: two SIDs and a supplement, then the operator. Its presence is
// the whole of what makes a CFF CID-keyed.
for k in 0 ..< 3 {
out[d + 5 * k] = 29
}
out[d + 15], out[d + 16] = 12, 30
d += 17
}
if own_matrix {
copy(out[d:], FN_CFF_MATRIX[:])
d += len(FN_CFF_MATRIX)
}
out[d] = 29
fn_w32(out, d + 1, u32(charset_off))
out[d + 5] = 15 // charset
out[d + 6] = 29
fn_w32(out, d + 7, u32(cs_off))
out[d + 11] = 17 // CharStrings
p += 5 + dict_len
fn_w16(out, p, 0) // String INDEX, empty
fn_w16(out, p + 2, 0) // Global Subr INDEX, empty
// charset, format 0: one SID (or CID) per glyph from glyph 1 up.
out[charset_off] = 0
for id, i in ids {
fn_w16(out, charset_off + 1 + 2 * i, id)
}
// CharStrings INDEX.
fn_w16(out, cs_off, u16(ng))
out[cs_off + 2] = 1
for i in 0 ..= ng {
out[cs_off + 3 + i] = u8(1 + i * len(FN_CS))
}
q := cs_off + 3 + ng + 1
for _ in 0 ..< ng {
copy(out[q:], FN_CS[:])
q += len(FN_CS)
}
return out
}
// ---------------------------------------------------------------------------
// Font fixtures
// ---------------------------------------------------------------------------
@(private = "file")
fn_dict :: proc(doc: ^Document, src: string) -> ^Dict {
d, _ := to_dict(cf_parse(doc, src))
return d
}
@(private = "file")
fn_load :: proc(t: ^testing.T, doc: ^Document, d: ^Dict, loc := #caller_location) -> ^Font {
f, err := font_load(doc, d)
testing.expectf(t, err == .None, "font_load: %v", err, loc = loc)
testing.expect(t, f != nil, "font_load returned no font", loc = loc)
return f
}
@(private = "file")
fn_font :: proc(t: ^testing.T, doc: ^Document, src: string, loc := #caller_location) -> ^Font {
return fn_load(t, doc, fn_dict(doc, src), loc)
}
// fn_simple builds a simple font whose descriptor carries `flags` and the
// program `prog` under `key`, which is how every embedded-program test here is
// set up.
@(private = "file")
fn_simple :: proc(t: ^testing.T, doc: ^Document, font_src, desc_src, key: string,
prog: []byte, loc := #caller_location) -> ^Font {
d := fn_dict(doc, font_src)
desc := fn_dict(doc, desc_src)
if prog != nil {
dict_set(desc, key, cf_stream(doc, "<< >>", prog))
}
dict_set(d, "FontDescriptor", desc)
return fn_load(t, doc, d, loc)
}
@(private = "file")
fn_expect_width :: proc(t: ^testing.T, f: ^Font, code: int, want: f64, what: string,
loc := #caller_location) {
if f == nil || code < 0 || code > 255 {
testing.expectf(t, false, "%s: no font", what, loc = loc)
return
}
got := f.code_width[code]
testing.expectf(t, got == want, "%s: width of code %d is %v, wanted %v",
what, code, got, want, loc = loc)
}
@(private = "file")
fn_expect_gid :: proc(t: ^testing.T, f: ^Font, code: int, want: u32, what: string,
loc := #caller_location) {
if f == nil || code < 0 || code > 255 {
testing.expectf(t, false, "%s: no font", what, loc = loc)
return
}
got := f.code_gid[code]
testing.expectf(t, got == want, "%s: gid for code %d is %d, wanted %d",
what, code, got, want, loc = loc)
}
// fn_decode runs a string through the font and hands back the codes. The
// dynamic array is the caller's to delete.
@(private = "file")
fn_decode :: proc(f: ^Font, s: string) -> [dynamic]Font_Code {
out := make([dynamic]Font_Code, 0, 8)
font_decode(f, transmute([]byte)s, &out)
return out
}
// ---------------------------------------------------------------------------
// Standard 14 metrics
// ---------------------------------------------------------------------------
@(test)
test_font_std14_widths :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// No /Widths, no /FontDescriptor: 9.6.2.2 lets a standard font omit both,
// and the AFM metrics are then the only widths there are.
f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
if f == nil { return }
testing.expect(t, f.std_index == STD_HELVETICA, "Helvetica was not recognised")
testing.expect(t, !f.symbolic, "Helvetica is not a symbolic font")
fn_expect_width(t, f, 'A', 667, "Helvetica A")
fn_expect_width(t, f, ' ', 278, "Helvetica space")
fn_expect_width(t, f, 'W', 944, "Helvetica W")
// StandardEncoding is what a font with no /Encoding falls back to.
testing.expect_value(t, f.enc_names['A'], "A")
testing.expect_value(t, f.enc_names[0xA5], "yen")
fn_expect_width(t, f, 0xA5, 556, "Helvetica yen")
}
@(test)
test_font_std14_subset_tag_and_alias :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A subset tag and a Monotype alias both have to resolve, or a font that
// is Times in everything but spelling loses every width it has.
f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+Times-Roman >>")
if f == nil { return }
testing.expect(t, f.std_index == STD_TIMES_ROMAN, "subset tag was not stripped")
fn_expect_width(t, f, 'A', 722, "Times A")
g := fn_font(t, doc, "<< /Type /Font /Subtype /TrueType /BaseFont /Arial,Bold >>")
if g == nil { return }
testing.expect(t, g.std_index == STD_HELVETICA_BOLD, "Arial,Bold is Helvetica-Bold")
}
@(test)
test_font_std14_symbol_builtin_encoding :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Symbol's built-in encoding is its own, not StandardEncoding: code 0x41
// is Alpha, and its width is Alpha's.
f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Symbol >>")
if f == nil { return }
testing.expect(t, f.symbolic, "Symbol is a symbolic font")
testing.expect_value(t, f.enc_names[0x41], "Alpha")
fn_expect_width(t, f, 0x41, 722, "Symbol Alpha")
fn_expect_width(t, f, ' ', 250, "Symbol space")
}
@(test)
test_font_widths_beat_metrics :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /Widths is the file speaking about its own font and wins over the AFM,
// even for a standard 14 name. Codes outside /FirstChar../LastChar fall
// through to the metrics.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica " +
"/FirstChar 65 /LastChar 67 /Widths [100 200 300] >>")
if f == nil { return }
fn_expect_width(t, f, 'A', 100, "widths[0]")
fn_expect_width(t, f, 'B', 200, "widths[1]")
fn_expect_width(t, f, 'C', 300, "widths[2]")
fn_expect_width(t, f, 'D', 722, "past LastChar falls back to the AFM")
}
@(test)
test_font_missing_width :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A font that is not one of the fourteen has no metrics to fall back on,
// so /MissingWidth is what an unlisted code is worth.
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Whatever " +
"/FirstChar 65 /LastChar 65 /Widths [400] >>",
"<< /Flags 32 /MissingWidth 555 >>", "", nil)
if f == nil { return }
fn_expect_width(t, f, 'A', 400, "listed code")
fn_expect_width(t, f, 'B', 555, "unlisted code")
fn_expect_width(t, f, 0, 555, "code 0")
// With no /MissingWidth the answer is zero, not a guess.
g := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Whatever " +
"/FirstChar 65 /LastChar 65 /Widths [400] >>",
"<< /Flags 32 >>", "", nil)
if g == nil { return }
fn_expect_width(t, g, 'B', 0, "no MissingWidth")
}
// ---------------------------------------------------------------------------
// Encodings
// ---------------------------------------------------------------------------
@(test)
test_font_base_encoding_and_differences :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /BaseEncoding replaces the built-in encoding wholesale; /Differences
// then overrides individual codes of it. Both have to be visible in the
// result, so the assertions below check a code from each.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding " +
"<< /BaseEncoding /WinAnsiEncoding /Differences [65 /bullet /Euro] >> >>")
if f == nil { return }
testing.expect_value(t, f.enc_names[65], "bullet")
testing.expect_value(t, f.enc_names[66], "Euro")
testing.expect_value(t, f.enc_names[67], "C") // WinAnsi, untouched
testing.expect_value(t, f.enc_names[0xA5], "yen") // WinAnsi 0xA5
fn_expect_width(t, f, 65, 350, "bullet")
fn_expect_width(t, f, 66, 556, "Euro")
fn_expect_width(t, f, 67, 722, "C")
}
@(test)
test_font_encoding_name_form :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /Encoding as a bare name. MacRoman and WinAnsi disagree about 0xA5,
// which is what makes this assertion able to tell them apart.
mac := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /MacRomanEncoding >>")
if mac == nil { return }
testing.expect_value(t, mac.enc_names[0xA5], "bullet")
fn_expect_width(t, mac, 0xA5, 350, "MacRoman 0xA5")
win := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")
if win == nil { return }
testing.expect_value(t, win.enc_names[0xA5], "yen")
}
@(test)
test_font_unknown_encoding_name_keeps_builtin :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// An /Encoding this library does not know is not a reason to reset the
// font to StandardEncoding -- for Symbol that would replace every glyph
// name with a Latin one the font does not have.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Symbol /Encoding /PDFDocEncoding >>")
if f == nil { return }
testing.expect_value(t, f.enc_names[0x41], "Alpha")
// A /Differences over the same unknown name still applies.
g := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Symbol /Encoding " +
"<< /BaseEncoding /Nonsense /Differences [65 /beta] >> >>")
if g == nil { return }
testing.expect_value(t, g.enc_names[0x41], "beta")
testing.expect_value(t, g.enc_names[0x42], "Beta")
}
@(test)
test_font_differences_malformed :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Codes outside 0..255, a real where an integer belongs, and a trailing
// name with no code before it. None of them may write out of bounds, and
// the entries around them still have to land.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding " +
"<< /Differences [-3 /A 300 /B 66.0 /X /Y (str) /Z] >> >>")
if f == nil { return }
testing.expect_value(t, f.enc_names[66], "X")
testing.expect_value(t, f.enc_names[67], "Y")
testing.expect_value(t, f.enc_names[68], "Z")
}
// ---------------------------------------------------------------------------
// TrueType: the symbolic rules (9.6.5.4)
// ---------------------------------------------------------------------------
@(test)
test_font_symbolic_truetype_symbol_cmap :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A (3,0) table mapping the bare byte 0x42 and the private-use 0xF041.
// Both conventions occur, so both codes have to resolve.
sym := fn_cmap4(doc, Fn_Seg{0x42, 0x42, 4}, Fn_Seg{0xF041, 0xF041, 3})
prog := fn_ttf(doc, 10, 1000, []Fn_Cmap_Rec{{3, 0, sym}})
// The /Encoding is present AND deliberately wrong: a symbolic font's
// codes do not go through it. If it were consulted, code 0x41 would look
// for "A" in a font whose only cmap is (3,0) and find nothing.
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Wingdings " +
"/Encoding /WinAnsiEncoding >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
testing.expect(t, f.symbolic, "the descriptor said symbolic")
testing.expect(t, f.embedded && !f.program_failed, "the program should have parsed")
fn_expect_gid(t, f, 0x42, 4, "bare code through the (3,0) cmap")
fn_expect_gid(t, f, 0x41, 3, "0xF000 + code through the (3,0) cmap")
fn_expect_gid(t, f, 0x43, 0, "a code the symbol cmap does not have")
}
@(test)
test_font_symbolic_truetype_mac_fallback :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// No (3,0) at all: a symbolic font then goes through the (1,0) Mac Roman
// table with the raw code, which is the other half of the rule.
prog := fn_ttf(doc, 10, 1000, []Fn_Cmap_Rec{{1, 0, fn_cmap0(doc, {0x41, 7}, {0xA5, 8})}})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Weird >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
fn_expect_gid(t, f, 0x41, 7, "(1,0) Mac Roman")
fn_expect_gid(t, f, 0xA5, 8, "(1,0) Mac Roman, high byte")
fn_expect_gid(t, f, 0x42, 0, "not in the Mac table")
}
@(test)
test_font_symbolic_truetype_symbol_beats_mac :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Both tables present and deliberately disagreeing. 9.6.5.4 orders them:
// the (3,0) symbol table is consulted first and the (1,0) Mac Roman one
// only answers what it left. A font carrying both is the ordinary shape
// of a Windows-authored symbol font, and reading them the other way round
// silently swaps every glyph on the page for a different one -- the sort
// of failure that renders and still looks wrong.
sym := fn_cmap4(doc, Fn_Seg{0x41, 0x41, 5}, Fn_Seg{0xF042, 0xF042, 6})
mac := fn_cmap0(doc, {0x41, 7}, {0x42, 8}, {0x43, 9})
prog := fn_ttf(doc, 12, 1000, []Fn_Cmap_Rec{{3, 0, sym}, {1, 0, mac}})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Both >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
testing.expect(t, f.symbolic, "the descriptor said symbolic")
fn_expect_gid(t, f, 0x41, 5, "the bare code in (3,0) beats (1,0)")
fn_expect_gid(t, f, 0x42, 6, "0xF000 + code in (3,0) also beats (1,0)")
fn_expect_gid(t, f, 0x43, 9, "and (1,0) still answers what (3,0) does not")
}
@(test)
test_font_nonsymbolic_truetype_name_to_unicode :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// The non-symbolic route: code -> name (through the encoding) -> Unicode
// (through the AGL) -> glyph (through the (3,1) cmap). /Differences puts
// "C" on code 65, so 65 must reach C's glyph and not A's.
uni := fn_cmap4(doc, Fn_Seg{'A', 'C', 1}) // A->1, B->2, C->3
prog := fn_ttf(doc, 10, 1000, []Fn_Cmap_Rec{{3, 1, uni}})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Plain /Encoding " +
"<< /BaseEncoding /WinAnsiEncoding /Differences [65 /C] >> >>",
"<< /Flags 32 >>", "FontFile2", prog)
if f == nil { return }
testing.expect(t, !f.symbolic, "flag 32 is nonsymbolic")
fn_expect_gid(t, f, 'A', 3, "/Differences put C on code 65")
fn_expect_gid(t, f, 'B', 2, "B through WinAnsi and the (3,1) cmap")
fn_expect_gid(t, f, 'C', 3, "C likewise")
fn_expect_gid(t, f, 'D', 0, "the cmap has no D")
}
@(test)
test_font_nonsymbolic_truetype_post_names :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A subsetter's names are not in the AGL, so the Unicode route fails and
// the `post` table is the only thing left that knows what "g99" is.
uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1})
post := fn_post2(doc, []string{".notdef", "one", "g99", "three"})
prog := fn_ttf(doc, 4, 1000, []Fn_Cmap_Rec{{3, 1, uni}}, post)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Sub /Encoding " +
"<< /Differences [65 /g99] >> >>",
"<< /Flags 32 >>", "FontFile2", prog)
if f == nil { return }
fn_expect_gid(t, f, 'A', 2, "the post table names glyph 2 g99")
}
@(test)
test_font_nonsymbolic_truetype_name_through_macroman :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// 9.6.5.4's third route, and the only one this font has: one cmap, (1,0),
// and no `post` names, which is the shape of anything authored on a Mac
// or run through a subsetter. The name has to be carried BACK through
// MacRomanEncoding to get the code that table is keyed by. WinAnsi puts
// "eacute" on 0xE9 and MacRomanEncoding puts it on 0x8E, so the glyph
// wanted is the one at 0x8E; looking the raw 0xE9 up in the same table
// finds MacRoman's "Egrave" instead -- a real glyph, the wrong letter,
// and a page that renders and is silently wrong.
mac := fn_cmap0(doc, {0x8E, 1}, {0xE9, 2}, {0x41, 3})
prog := fn_ttf(doc, 4, 1000, []Fn_Cmap_Rec{{1, 0, mac}})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /MacOnly " +
"/Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile2", prog)
if f == nil { return }
testing.expect(t, !f.symbolic, "flag 32 is nonsymbolic")
fn_expect_gid(t, f, 0xE9, 1, "WinAnsi eacute through MacRoman's 0x8E")
// The other direction, so that a fix which merely offsets the code cannot
// pass: WinAnsi 0xC8 is "Egrave", which MacRomanEncoding puts on 0xE9.
fn_expect_gid(t, f, 0xC8, 2, "WinAnsi Egrave through MacRoman's 0xE9")
fn_expect_gid(t, f, 0x41, 3, "ASCII, where the two encodings agree")
fn_expect_gid(t, f, 0xEA, 0, "a code neither route reaches")
}
@(test)
test_font_truetype_no_cmap_is_gid :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A stripped subset with no cmap at all. Every viewer reads the code as a
// glyph index here, and there is nothing else it could mean.
prog := fn_ttf(doc, 6, 1000, nil)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Sub /Encoding /WinAnsiEncoding >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
fn_expect_gid(t, f, 3, 3, "code as glyph index")
fn_expect_gid(t, f, 5, 5, "code as glyph index")
fn_expect_gid(t, f, 6, 0, "past the glyph count there is no glyph")
fn_expect_gid(t, f, 200, 0, "far past the glyph count")
}
@(test)
test_font_truetype_units_per_em :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Glyph space for a TrueType font is 1/unitsPerEm, not 1/1000: a 2048-unit
// font drawn through a thousandths matrix comes out twice too large.
prog := fn_ttf(doc, 4, 2048, nil)
f := fn_simple(t, doc, "<< /Type /Font /Subtype /TrueType /BaseFont /Big >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
m := font_matrix(f)
testing.expect_value(t, m.a, 1.0 / 2048.0)
testing.expect_value(t, m.d, 1.0 / 2048.0)
testing.expect(t, !font_is_type3(f), "a TrueType font is not Type3")
}
@(test)
test_font_truetype_hmtx_advance :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// No /Widths and no /MissingWidth: the program's own advances are the
// last thing left, and they have to be scaled out of font units into the
// thousandths every PDF width is in. 1024/2048 of an em is 500.
uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1})
prog := fn_ttf(doc, 3, 2048, []Fn_Cmap_Rec{{3, 1, uni}}, nil, []u16{0, 1024, 512})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /NoWidths /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile2", prog)
if f == nil { return }
fn_expect_gid(t, f, 'A', 1, "A is glyph 1")
fn_expect_width(t, f, 'A', 500, "hmtx advance scaled to 1/1000")
}
// ---------------------------------------------------------------------------
// CFF programs
// ---------------------------------------------------------------------------
@(test)
test_font_cff_simple_by_name :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A name-keyed Type1C: SIDs 34, 35 and 36 are "A", "B" and "C" among the
// CFF standard strings, so the charset names glyphs 1..3 after them and
// the encoding's names are what find them.
prog := fn_cff(doc, []u16{34, 35, 36}, false)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Emb /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile3", prog)
if f == nil { return }
testing.expect(t, f.cff != nil, "the CFF should have parsed")
testing.expect(t, !f.program_failed, "no failure expected")
fn_expect_gid(t, f, 'A', 1, "charset SID 34 is A")
fn_expect_gid(t, f, 'C', 3, "charset SID 36 is C")
fn_expect_gid(t, f, 'D', 0, "the font has no D")
// A CFF's own FontMatrix is the glyph space, and this one has none, so it
// is the 1/1000 default rather than anything the sfnt would have said.
testing.expect_value(t, font_matrix(f).a, 0.001)
p := path_make()
defer path_destroy(&p)
err := font_glyph_path(f, 1, &p)
testing.expectf(t, err == .None, "font_glyph_path: %v", err)
testing.expect(t, !path_is_empty(&p), "the charstring draws a point")
}
@(test)
test_font_cff_cid_charset :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A CID-keyed CFF: its charset holds CIDs, not name SIDs, so the CID a
// string decodes to has to be looked up in it rather than used as a glyph
// index. Glyph 2 is CID 20 here, and reading CID 20 as a GID would draw
// glyph 20, which does not exist.
prog := fn_cff(doc, []u16{10, 20, 30}, true)
cid := fn_dict(doc, "<< /Type /Font /Subtype /CIDFontType0 /DW 600 >>")
desc := fn_dict(doc, "<< /Flags 4 >>")
dict_set(desc, "FontFile3", cf_stream(doc, "<< /Subtype /CIDFontType0C >>", prog))
dict_set(cid, "FontDescriptor", desc)
f := fn_type0(t, doc, "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H >>", cid)
if f == nil { return }
testing.expect(t, f.cff != nil && f.cff.is_cid, "a CID-keyed CFF was expected")
codes := fn_decode(f, "\x00\x14\x00\x0a\x00\x63")
defer delete(codes)
if !testing.expect(t, len(codes) == 3, "three two-byte codes") { return }
testing.expect_value(t, codes[0].cid, 20)
testing.expect_value(t, codes[0].gid, 2)
testing.expect_value(t, codes[1].cid, 10)
testing.expect_value(t, codes[1].gid, 1)
testing.expect_value(t, codes[2].gid, 0) // CID 99 is in no charset entry
}
@(test)
test_font_opentype_cff :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /FontFile3 /OpenType with CFF outlines: an sfnt wrapper with a `CFF `
// table and no `glyf`. Both readers stay live -- the wrapper answers
// character lookups and the CFF draws -- so the font must end up with the
// CFF's matrix and a working glyph path.
cff := fn_cff(doc, []u16{34, 35}, false)
uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1})
otf := fn_sfnt(doc, []Fn_Table{
{"head", fn_head(doc, 2048)},
{"maxp", fn_maxp(doc, 3)},
{"cmap", fn_cmap(doc, Fn_Cmap_Rec{3, 1, uni})},
{"CFF ", cff},
}, 0x4F54544F)
d := fn_dict(doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Otf /Encoding /WinAnsiEncoding >>")
desc := fn_dict(doc, "<< /Flags 32 >>")
dict_set(desc, "FontFile3", cf_stream(doc, "<< /Subtype /OpenType >>", otf))
dict_set(d, "FontDescriptor", desc)
f := fn_load(t, doc, d)
if f == nil { return }
testing.expect(t, f.ttf != nil, "the sfnt wrapper should have parsed")
testing.expect(t, f.cff != nil, "its CFF table should have parsed too")
testing.expect(t, !f.program_failed, "no failure expected")
// 1/2048 would be the sfnt's answer; the CFF's own matrix is the right one.
testing.expect_value(t, font_matrix(f).a, 0.001)
fn_expect_gid(t, f, 'A', 1, "through the sfnt cmap")
p := path_make()
defer path_destroy(&p)
testing.expect_value(t, font_glyph_path(f, 1, &p), Error.None)
testing.expect(t, !path_is_empty(&p), "the CFF charstring draws")
}
@(test)
test_font_program_sniffed_not_declared :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A bare CFF filed under /FontFile2, which says TrueType. The bytes are
// unambiguous and the dictionary is wrong; producers do this and every
// viewer believes the bytes.
prog := fn_cff(doc, []u16{34}, false)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Lying /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile2", prog)
if f == nil { return }
testing.expect(t, f.cff != nil, "the CFF should have been recognised by its header")
testing.expect(t, f.ttf == nil, "it is not a TrueType font")
fn_expect_gid(t, f, 'A', 1, "and its glyph is still reachable")
}
// ---------------------------------------------------------------------------
// A program that will not parse
// ---------------------------------------------------------------------------
@(test)
test_font_corrupt_program_keeps_widths :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// The rule the whole file is built around: a font program that cannot be
// read costs the glyphs and nothing else. The widths, the encoding and the
// Unicode values all still have to come out, because the page still has to
// lay out and extract.
junk := fn_alloc(doc, 64)
for i in 0 ..< len(junk) {
junk[i] = u8(i * 7 + 3)
}
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Broken /FirstChar 65 " +
"/LastChar 66 /Widths [321 654] /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 /MissingWidth 7 >>", "FontFile2", junk)
if f == nil { return }
testing.expect(t, f.program_failed, "program_failed must be set")
testing.expect(t, !f.embedded, "nothing was embedded that could be read")
testing.expect(t, f.ttf == nil && f.cff == nil && f.t1 == nil, "no reader should be live")
fn_expect_width(t, f, 'A', 321, "widths survive a broken program")
fn_expect_width(t, f, 'B', 654, "widths survive a broken program")
fn_expect_width(t, f, 'C', 7, "and so does /MissingWidth")
testing.expect_value(t, f.enc_names['A'], "A")
codes := fn_decode(f, "AB")
defer delete(codes)
if !testing.expect(t, len(codes) == 2, "two codes") { return }
testing.expect_value(t, codes[0].unicode, 'A')
testing.expect_value(t, codes[0].width, 321)
p := path_make()
defer path_destroy(&p)
testing.expect_value(t, font_glyph_path(f, 1, &p), Error.No_Glyph)
testing.expect(t, path_is_empty(&p), "nothing may be drawn")
testing.expect(t, doc.is_damaged, "a repair should have been recorded")
}
@(test)
test_font_empty_program :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
"<< /Flags 32 >>", "FontFile", nil)
if f == nil { return }
// No /FontFile at all is not a failure: it is a non-embedded font, and
// the flag has to distinguish the two or a caller cannot tell a missing
// program from a broken one.
testing.expect(t, !f.program_failed, "no program is not a failed program")
testing.expect(t, !f.embedded, "and it is not embedded either")
fn_expect_width(t, f, 'A', 667, "the AFM still measures it")
// A /FontFile that is present but decodes to nothing is a different case:
// the file said there was a program and there is not.
gd := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
gdesc := fn_dict(doc, "<< /Flags 32 >>")
dict_set(gdesc, "FontFile", cf_stream(doc, "<< >>", nil))
dict_set(gd, "FontDescriptor", gdesc)
g := fn_load(t, doc, gd)
if g == nil { return }
testing.expect(t, g.program_failed, "an empty program is a failed one")
fn_expect_width(t, g, 'A', 667, "and the metrics still measure it")
}
// ---------------------------------------------------------------------------
// Composite fonts
// ---------------------------------------------------------------------------
@(private = "file")
fn_type0 :: proc(t: ^testing.T, doc: ^Document, font_src: string, cid: ^Dict,
loc := #caller_location) -> ^Font {
d := fn_dict(doc, font_src)
a := new_array(doc.allocator)
array_push(a, cid)
dict_set(d, "DescendantFonts", a)
return fn_load(t, doc, d, loc)
}
@(test)
test_font_type0_identity_h :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H " +
"/DescendantFonts [<< /Subtype /CIDFontType2 /DW 1000 >>] >>")
if f == nil { return }
testing.expect(t, f.kind == .Type0, "Type0")
testing.expect(t, !f.vertical, "Identity-H is horizontal")
// Two bytes per code, big-endian, and the CID is the code.
codes := fn_decode(f, "\x00\x41\x01\x02\xff")
defer delete(codes)
if !testing.expect(t, len(codes) == 3, "two whole codes and a truncated one") { return }
testing.expect_value(t, codes[0].code, 0x0041)
testing.expect_value(t, codes[0].cid, 0x0041)
testing.expect_value(t, codes[0].gid, 0x0041)
testing.expect_value(t, codes[0].nbytes, 2)
testing.expect_value(t, codes[1].code, 0x0102)
testing.expect_value(t, codes[1].width, 1000)
// A trailing half-code still consumes the byte, or the caller loops.
testing.expect_value(t, codes[2].nbytes, 1)
}
@(test)
test_font_type0_w_both_forms :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Both /W forms in one array: "c [w ...]" for CIDs 1 and 2, then
// "c_first c_last w" for 5..7. Everything else is /DW.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 1000 /W [1 [100 200] 5 7 300] >>] >>")
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 1), 100)
testing.expect_value(t, font_cid_width(f, 2), 200)
testing.expect_value(t, font_cid_width(f, 3), 1000)
testing.expect_value(t, font_cid_width(f, 4), 1000)
testing.expect_value(t, font_cid_width(f, 5), 300)
testing.expect_value(t, font_cid_width(f, 6), 300)
testing.expect_value(t, font_cid_width(f, 7), 300)
testing.expect_value(t, font_cid_width(f, 8), 1000)
testing.expect_value(t, font_cid_width(f, 0), 1000)
}
@(test)
test_font_type0_w_overlapping :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Overlapping ranges are malformed but occur: a broad run with a narrow
// correction written over part of it. The correction is the range that
// starts later, and it is the one that has to win.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 50 /W [1 10 300 4 6 900] >>] >>")
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 3), 300)
testing.expect_value(t, font_cid_width(f, 4), 900)
testing.expect_value(t, font_cid_width(f, 6), 900)
testing.expect_value(t, font_cid_width(f, 7), 300)
testing.expect_value(t, font_cid_width(f, 11), 50)
}
@(test)
test_font_type0_w_malformed :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A name where a number belongs, a range that runs backwards, and a CID
// past the 16-bit ceiling. None may be fatal and none may swallow the
// entries after them.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 7 /W [/Bogus 20 18 400 99999 [1] 30 [600]] >>] >>")
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 19), 400)
testing.expect_value(t, font_cid_width(f, 30), 600)
testing.expect_value(t, font_cid_width(f, 31), 7)
}
@(test)
test_font_type0_cid_to_gid_stream :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /CIDToGIDMap as a stream of big-endian u16 indexed by CID.
m := fn_alloc(doc, 6)
fn_w16(m, 0, 0)
fn_w16(m, 2, 5)
fn_w16(m, 4, 7)
cid := fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 1000 >>")
dict_set(cid, "CIDToGIDMap", cf_stream(doc, "<< >>", m))
f := fn_type0(t, doc, "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H >>", cid)
if f == nil { return }
testing.expect_value(t, len(f.cid_to_gid), 3)
testing.expect_value(t, font_cid_gid(f, 0), 0)
testing.expect_value(t, font_cid_gid(f, 1), 5)
testing.expect_value(t, font_cid_gid(f, 2), 7)
// Past the end of the map the CID has no glyph -- the table is not an
// "identity from here on" promise.
testing.expect_value(t, font_cid_gid(f, 3), 0)
codes := fn_decode(f, "\x00\x02")
defer delete(codes)
if !testing.expect(t, len(codes) == 1, "one code") { return }
testing.expect_value(t, codes[0].gid, 7)
}
@(test)
test_font_type0_cid_to_gid_identity :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /CIDToGIDMap /Identity >>] >>")
if f == nil { return }
testing.expect_value(t, len(f.cid_to_gid), 0)
testing.expect_value(t, font_cid_gid(f, 1234), 1234)
}
@(test)
test_font_type0_vertical :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Identity-V: the writing mode comes from the CMap, not from the font
// dictionary. With no /W2 the displacement is DW2[1] and the position
// vector is (w0/2, DW2[0]).
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-V /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 800 >>] >>")
if f == nil { return }
testing.expect(t, f.vertical, "Identity-V is vertical")
testing.expect_value(t, f.default_w1, -1000)
testing.expect_value(t, f.default_vy, 880)
codes := fn_decode(f, "\x00\x41")
defer delete(codes)
if !testing.expect(t, len(codes) == 1, "one code") { return }
testing.expect_value(t, codes[0].width, -1000)
testing.expect_value(t, codes[0].vx, 400)
testing.expect_value(t, codes[0].vy, 880)
}
@(test)
test_font_type0_w2 :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /W2's two forms. Its entries are triples: w1y, then the position
// vector. /DW2 replaces the defaults for everything they do not cover.
// The two runs are written out of order, which /W2 is allowed to be and
// which a binary search over them is not.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-V /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 500 /DW2 [700 -900] " +
"/W2 [5 6 -820 120 920 1 [-800 100 900 -810 110 910]] >>] >>")
if f == nil { return }
testing.expect_value(t, f.default_vy, 700)
testing.expect_value(t, f.default_w1, -900)
w, vx, vy := font_cid_vmetric(f, 1, 500)
testing.expect_value(t, w, -800)
testing.expect_value(t, vx, 100)
testing.expect_value(t, vy, 900)
w, vx, vy = font_cid_vmetric(f, 2, 500)
testing.expect_value(t, w, -810)
testing.expect_value(t, vx, 110)
w, vx, vy = font_cid_vmetric(f, 6, 500)
testing.expect_value(t, w, -820)
testing.expect_value(t, vx, 120)
testing.expect_value(t, vy, 920)
// Uncovered: the /DW2 defaults, with the position vector at half the
// horizontal width.
w, vx, vy = font_cid_vmetric(f, 40, 500)
testing.expect_value(t, w, -900)
testing.expect_value(t, vx, 250)
testing.expect_value(t, vy, 700)
}
@(test)
test_font_type0_embedded_cmap :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// An embedded CMap stream with a one-byte and a two-byte codespace. A
// composite font's codes are not all two bytes wide, and a decoder that
// assumes they are reads the rest of the string at the wrong offset.
src := `/CIDInit /ProcSet findresource begin
begincmap
2 begincodespacerange
<00> <7f>
<8140> <9ffc>
endcodespacerange
2 begincidrange
<20> <7e> 1
<8140> <8145> 100
endcidrange
endcmap
end`
d := fn_dict(doc, "<< /Type /Font /Subtype /Type0 /BaseFont /Mixed >>")
dict_set(d, "Encoding", cf_stream(doc, "<< >>", transmute([]byte)src))
a := new_array(doc.allocator)
array_push(a, fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 1000 >>"))
dict_set(d, "DescendantFonts", a)
f := fn_load(t, doc, d)
if f == nil { return }
codes := fn_decode(f, "A\x81\x42B")
defer delete(codes)
if !testing.expect(t, len(codes) == 3, "one byte, two bytes, one byte") { return }
testing.expect_value(t, codes[0].nbytes, 1)
testing.expect_value(t, codes[0].cid, 1 + 0x41 - 0x20)
testing.expect_value(t, codes[1].nbytes, 2)
testing.expect_value(t, codes[1].code, 0x8142)
testing.expect_value(t, codes[1].cid, 102)
testing.expect_value(t, codes[2].nbytes, 1)
}
@(test)
test_font_type0_degrades :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// No /DescendantFonts and no /Encoding. Neither is recoverable, but the
// font still has to decode text: /ToUnicode does not need either of them,
// and extraction is the last thing to give up.
f := fn_font(t, doc, "<< /Type /Font /Subtype /Type0 /BaseFont /Nothing >>")
if f == nil { return }
testing.expect(t, f.cmap != nil, "Identity-H stands in for a missing /Encoding")
testing.expect(t, doc.is_damaged, "and the substitution is a repair")
codes := fn_decode(f, "\x00\x41\x00\x42")
defer delete(codes)
testing.expect_value(t, len(codes), 2)
}
@(test)
test_font_type0_predefined_fallback :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A CJK CMap this library does not ship. cmap.odin substitutes a two-byte
// identity and flags it; what matters here is that the font still
// segments its string two bytes at a time instead of one.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /UniJIS-UCS2-H /DescendantFonts " +
"[<< /Subtype /CIDFontType0 /DW 1000 >>] >>")
if f == nil { return }
testing.expect(t, f.cmap != nil && f.cmap.predefined_fallback, "the fallback flag")
codes := fn_decode(f, "\x30\x42\x30\x44")
defer delete(codes)
testing.expect_value(t, len(codes), 2)
testing.expect_value(t, codes[0].code, 0x3042)
// A "-V" name is vertical even when the mapping itself is a substitute.
g := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /UniJIS-UCS2-V /DescendantFonts " +
"[<< /Subtype /CIDFontType0 >>] >>")
if g == nil { return }
testing.expect(t, g.vertical, "a -V CMap is vertical")
}
// ---------------------------------------------------------------------------
// Type3 (9.6.4)
// ---------------------------------------------------------------------------
@(test)
test_font_type3_matrix :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A Type3 font's /FontMatrix is its own and is NOT 1/1000. This one draws
// in hundredths, which is a scale factor of ten against the thousandths
// every other font uses: honouring it is the difference between text at
// the right size and text ten times too small.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0 0.01 0 0] " +
"/FontBBox [0 0 100 100] /FirstChar 97 /LastChar 98 /Widths [75 50] " +
"/CharProcs << >> /Encoding << /Differences [97 /square /tri] >> >>")
if f == nil { return }
testing.expect(t, font_is_type3(f), "font_is_type3")
m := font_matrix(f)
testing.expect_value(t, m.a, 0.01)
testing.expect_value(t, m.d, 0.01)
testing.expect_value(t, m.e, 0.0)
testing.expect_value(t, f.enc_names[97], "square")
testing.expect_value(t, f.enc_names[98], "tri")
// Widths are in that same glyph space, untouched: 75 of these units is
// 0.75 of a text space unit, which only the matrix can say.
fn_expect_width(t, f, 97, 75, "Type3 width")
fn_expect_width(t, f, 98, 50, "Type3 width")
fn_expect_width(t, f, 99, 0, "an unlisted Type3 code")
// A Type3 glyph is a content stream, so there is no outline to hand back.
p := path_make()
defer path_destroy(&p)
testing.expect_value(t, font_glyph_path(f, 97, &p), Error.No_Glyph)
}
@(test)
test_font_type3_char_procs :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
procs := fn_dict(doc, "<< >>")
dict_set(procs, "square", cf_stream(doc, "<< >>", cf_bytes("0 0 10 10 re f")))
d := fn_dict(doc,
"<< /Type /Font /Subtype /Type3 /FontMatrix [0.001 0 0 0.001 0 0] " +
"/Encoding << /Differences [65 /square] >> >>")
dict_set(d, "CharProcs", procs)
dict_set(d, "Resources", fn_dict(doc, "<< /ProcSet [/PDF] >>"))
f := fn_load(t, doc, d)
if f == nil { return }
testing.expect(t, f.char_procs != nil, "/CharProcs")
testing.expect(t, f.t3_resources != nil, "/Resources")
// The interpreter finds a glyph by the NAME the encoding gives its code,
// which is the only route a Type3 font has.
testing.expect(t, font_type3_proc(f, 65) != nil, "code 65 is /square")
testing.expect(t, font_type3_proc(f, 66) == nil, "code 66 names nothing")
testing.expect(t, font_type3_proc(f, 9999) == nil, "an impossible code")
// A Type3 font has no program to index, so the code stands in as the
// glyph id: the interpreter still has to carry something through to the
// device, and the code is the only identifier there is.
codes := fn_decode(f, "AB")
defer delete(codes)
if !testing.expect(t, len(codes) == 2, "two codes") { return }
testing.expect_value(t, codes[0].gid, 65)
testing.expect_value(t, codes[1].gid, 66)
}
@(test)
test_font_type3_bad_matrix :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A singular /FontMatrix would collapse every glyph to a line. It is not
// recoverable as written, so the usual thousandths stand in and a repair
// is recorded.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type3 /FontMatrix [0 0 0 0 0 0] /CharProcs << >> >>")
if f == nil { return }
testing.expect_value(t, font_matrix(f).a, 0.001)
testing.expect(t, doc.is_damaged, "a repair should have been recorded")
// A /FontMatrix that is too short is the same case.
g := fn_font(t, doc,
"<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0] /CharProcs << >> >>")
if g == nil { return }
testing.expect_value(t, font_matrix(g).a, 0.001)
}
@(test)
test_font_kind_guessed :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A font dictionary with no /Subtype is malformed and perfectly readable:
// its other keys say what it is.
f := fn_font(t, doc, "<< /Type /Font /CharProcs << >> /FontMatrix [1 0 0 1 0 0] >>")
if f == nil { return }
testing.expect(t, f.kind == .Type3, "/CharProcs makes it Type3")
testing.expect_value(t, font_matrix(f).a, 1.0)
g := fn_font(t, doc,
"<< /Type /Font /DescendantFonts [<< /Subtype /CIDFontType2 >>] >>")
if g == nil { return }
testing.expect(t, g.kind == .Type0, "/DescendantFonts makes it Type0")
h := fn_font(t, doc, "<< /Type /Font /BaseFont /Helvetica >>")
if h == nil { return }
testing.expect(t, h.kind == .Type1, "and a simple font is the default")
fn_expect_width(t, h, 'A', 667, "which still measures")
}
// ---------------------------------------------------------------------------
// Decoding
// ---------------------------------------------------------------------------
@(test)
test_font_is_space :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Word spacing applies to a single-byte code 32 and to nothing else
// (9.3.3). A two-byte 0x0020 is an ordinary CID, and taking it for a
// space stretches every CJK line that happens to contain one.
simple := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
if simple == nil { return }
sc := fn_decode(simple, "A B")
defer delete(sc)
if !testing.expect(t, len(sc) == 3, "three codes") { return }
testing.expect(t, !sc[0].is_space, "A is not a space")
testing.expect(t, sc[1].is_space, "a one-byte 32 IS a space")
testing.expect(t, !sc[2].is_space, "B is not a space")
comp := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 >>] >>")
if comp == nil { return }
cc := fn_decode(comp, "\x00\x20\x20\x20")
defer delete(cc)
if !testing.expect(t, len(cc) == 2, "two two-byte codes") { return }
testing.expect_value(t, cc[0].code, 0x0020)
testing.expect(t, !cc[0].is_space, "a two-byte 32 is NOT a space")
testing.expect(t, !cc[1].is_space, "nor is 0x2020")
// The half of 9.3.3 that the rule is actually written for: word spacing
// applies to "a composite font that defines code 32 as a single-byte
// code" too. The test above passes just as well if the code keys on the
// font being composite; only this one pins it to the CODE's width, which
// is what the format says. A one-byte codespace is what every
// /Encoding-with-a-one-byte-range CMap gives, and a Type0 font over such
// a CMap has to justify its spaces like any other.
src := `begincmap
1 begincodespacerange
<00> <ff>
endcodespacerange
1 begincidrange
<00> <ff> 0
endcidrange
endcmap`
d := fn_dict(doc, "<< /Type /Font /Subtype /Type0 /BaseFont /OneByte >>")
dict_set(d, "Encoding", cf_stream(doc, "<< >>", transmute([]byte)src))
a := new_array(doc.allocator)
array_push(a, fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 500 >>"))
dict_set(d, "DescendantFonts", a)
one := fn_load(t, doc, d)
if one == nil { return }
oc := fn_decode(one, "A B")
defer delete(oc)
if !testing.expect(t, len(oc) == 3, "three one-byte codes") { return }
testing.expect_value(t, oc[1].nbytes, 1)
testing.expect(t, !oc[0].is_space, "A is not a space")
testing.expect(t, oc[1].is_space, "a ONE-byte 32 is a space even in a Type0 font")
testing.expect(t, !oc[2].is_space, "B is not a space")
}
@(test)
test_font_decode_fills_everything :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /FirstChar 65 " +
"/LastChar 66 /Widths [123 456] >>")
if f == nil { return }
codes := fn_decode(f, "AB")
defer delete(codes)
if !testing.expect(t, len(codes) == 2, "two codes") { return }
testing.expect_value(t, codes[0].code, 'A')
testing.expect_value(t, codes[0].cid, 'A')
testing.expect_value(t, codes[0].nbytes, 1)
testing.expect_value(t, codes[0].width, 123)
testing.expect_value(t, codes[0].unicode, 'A')
testing.expect_value(t, codes[1].width, 456)
testing.expect_value(t, codes[1].unicode, 'B')
}
@(test)
test_font_decode_clears_output :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
if f == nil { return }
out := make([dynamic]Font_Code, 0, 8)
defer delete(out)
font_decode(f, cf_bytes("ABC"), &out)
testing.expect_value(t, len(out), 3)
// The buffer is meant to be reused across every string on a page, so the
// second call must not append to the first.
font_decode(f, cf_bytes("X"), &out)
testing.expect_value(t, len(out), 1)
testing.expect_value(t, out[0].code, 'X')
font_decode(f, nil, &out)
testing.expect_value(t, len(out), 0)
}
@(test)
test_font_to_unicode_wins :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /ToUnicode is the producer saying what it meant, and it beats the glyph
// name: a subset font's names are frequently lies and its /ToUnicode is
// the only thing that makes the text extractable.
tu := `begincmap
1 begincodespacerange <00> <ff> endcodespacerange
2 beginbfchar
<41> <2660>
<42> <0066006600690065>
endbfchar
endcmap`
d := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
dict_set(d, "ToUnicode", cf_stream(doc, "<< >>", transmute([]byte)tu))
f := fn_load(t, doc, d)
if f == nil { return }
testing.expect(t, f.to_unicode != nil, "/ToUnicode should have loaded")
codes := fn_decode(f, "ABC")
defer delete(codes)
if !testing.expect(t, len(codes) == 3, "three codes") { return }
testing.expect_value(t, codes[0].unicode, '♠')
// A code that stands for several characters reports the first of them;
// the whole string is cmap_lookup_text's to give.
testing.expect_value(t, codes[1].unicode, 'f')
// A code /ToUnicode says nothing about falls back to the glyph name.
testing.expect_value(t, codes[2].unicode, 'C')
}
@(test)
test_font_unicode_routes :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// With no /ToUnicode the glyph name is the route, through the AGL. A
// "uniXXXX" name works the same way, and a name that means nothing at all
// leaves the code with no character rather than a guessed one.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding " +
"<< /Differences [65 /Alpha 66 /uni0416 67 /g42] >> >>")
if f == nil { return }
codes := fn_decode(f, "ABC")
defer delete(codes)
if !testing.expect(t, len(codes) == 3, "three codes") { return }
testing.expect_value(t, codes[0].unicode, 'Α')
testing.expect_value(t, codes[1].unicode, 'Ж')
// "g42" is not a name the AGL knows. The code is a printable ASCII byte
// in a non-symbolic font, which is the last-resort reading.
testing.expect_value(t, codes[2].unicode, 'C')
}
@(test)
test_font_symbolic_unicode_not_guessed :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// The ASCII last resort must not fire for a symbolic font: code 0x41 in
// a dingbat font is a dingbat, and calling it "A" puts nonsense into
// extracted text rather than leaving a gap that a reader can see.
sym := fn_cmap4(doc, Fn_Seg{0xF041, 0xF041, 3})
prog := fn_ttf(doc, 6, 1000, []Fn_Cmap_Rec{{3, 0, sym}})
// The name is deliberately not one std_font_lookup recognises: matching
// ZapfDingbats would hand the font that AFM's encoding and the code WOULD
// then have a name, which is a different test.
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Icons >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
codes := fn_decode(f, "A")
defer delete(codes)
if !testing.expect(t, len(codes) == 1, "one code") { return }
testing.expect_value(t, codes[0].gid, 3)
testing.expect_value(t, codes[0].unicode, rune(-1))
}
@(test)
test_font_unicode_from_post_names :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A symbolic font with no PDF encoding still has names in its `post`
// table, and they are the only thing that can make its text extractable.
sym := fn_cmap4(doc, Fn_Seg{0xF041, 0xF041, 2})
post := fn_post2(doc, []string{".notdef", "one", "Sigma"})
prog := fn_ttf(doc, 3, 1000, []Fn_Cmap_Rec{{3, 0, sym}}, post)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Sym >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
codes := fn_decode(f, "A")
defer delete(codes)
if !testing.expect(t, len(codes) == 1, "one code") { return }
testing.expect_value(t, codes[0].gid, 2)
testing.expect_value(t, codes[0].unicode, rune(0x03A3))
}
// ---------------------------------------------------------------------------
// Caching, arguments and clamping
// ---------------------------------------------------------------------------
@(test)
test_font_cached_on_document :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A page that selects the same font two hundred times must parse its
// embedded program once, so the cache is keyed by the dictionary itself.
d := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
a := fn_load(t, doc, d)
b := fn_load(t, doc, d)
testing.expect(t, a == b && a != nil, "the same dictionary must yield the same Font")
other := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
c := fn_load(t, doc, other)
testing.expect(t, c != a, "a different dictionary is a different font")
}
@(test)
test_font_nil_arguments :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
_, e1 := font_load(nil, nil)
testing.expect_value(t, e1, Error.Invalid_Argument)
_, e2 := font_load(doc, nil)
testing.expect_value(t, e2, Error.Invalid_Argument)
testing.expect_value(t, font_matrix(nil).a, 0.001)
testing.expect(t, !font_is_type3(nil), "nil is not a Type3 font")
testing.expect(t, font_type3_proc(nil, 0) == nil, "nil has no char procs")
p := path_make()
defer path_destroy(&p)
testing.expect_value(t, font_glyph_path(nil, 0, &p), Error.Invalid_Argument)
f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
testing.expect_value(t, font_glyph_path(f, 0, nil), Error.Invalid_Argument)
// A glyph index wider than the u16 every program indexes with.
testing.expect_value(t, font_glyph_path(f, 0x10000, &p), Error.No_Glyph)
// A nil output buffer is a caller mistake, not a crash.
font_decode(f, cf_bytes("A"), nil)
out := make([dynamic]Font_Code, 0, 4)
defer delete(out)
font_decode(nil, cf_bytes("A"), &out)
testing.expect_value(t, len(out), 0)
}
@(test)
test_font_width_clamping :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A width from a file is attacker-controlled and ends up multiplied into
// the text matrix. Nothing below the device line may see an infinity, so
// an absurd width is clamped rather than carried.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Whatever /FirstChar 65 " +
"/LastChar 67 /Widths [1e300 -1e300 12] >>")
if f == nil { return }
testing.expect_value(t, f.code_width['A'], f64(MAX_GLYPH_WIDTH))
testing.expect_value(t, f.code_width['B'], -f64(MAX_GLYPH_WIDTH))
testing.expect_value(t, f.code_width['C'], 12)
testing.expect(t, is_finite(f.code_width['A']), "no infinity may survive")
g := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 1e300 /W [1 1 -1e300] >>] >>")
if g == nil { return }
testing.expect_value(t, font_cid_width(g, 1), -f64(MAX_GLYPH_WIDTH))
testing.expect_value(t, font_cid_width(g, 2), f64(MAX_GLYPH_WIDTH))
}
@(test)
test_font_descriptor_flags :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// 9.8.2, table 121: FixedPitch is bit 1, Serif bit 2, Symbolic bit 3,
// Nonsymbolic bit 6, Italic bit 7. Producers set Symbolic and Nonsymbolic
// together often enough that the pair has to be read as a whole, and a
// font claiming both is claiming nothing.
f := fn_simple(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /X >>",
"<< /Flags 67 >>", "", nil) // 1 | 2 | 64
if f == nil { return }
testing.expect(t, f.fixed_pitch, "FixedPitch")
testing.expect(t, f.serif, "Serif")
testing.expect(t, f.italic, "Italic")
testing.expect(t, !f.symbolic, "not symbolic")
g := fn_simple(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /X >>",
"<< /Flags 4 >>", "", nil)
if g == nil { return }
testing.expect(t, g.symbolic, "Symbolic alone")
h := fn_simple(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /X >>",
"<< /Flags 36 >>", "", nil) // 4 | 32
if h == nil { return }
testing.expect(t, !h.symbolic, "Symbolic and Nonsymbolic together is neither")
}
@(test)
test_font_symbolic_truetype_unicode_only_cmap :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A subset that sets the Symbolic flag and then ships a (3,1) cmap and
// nothing else. It is one of the most common shapes there is -- TeX and
// several web-to-PDF tools emit it -- and both prescribed routes are dead
// ends for it: there is no (3,0), no (1,0), and no /Encoding to give the
// code a name. Reading the code as a character is what is left.
uni := fn_cmap4(doc, Fn_Seg{'A', 'C', 11})
prog := fn_ttf(doc, 20, 1000, []Fn_Cmap_Rec{{3, 1, uni}})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Subset >>",
"<< /Flags 4 >>", "FontFile2", prog)
if f == nil { return }
testing.expect(t, f.symbolic, "the descriptor said symbolic")
fn_expect_gid(t, f, 'A', 11, "the code read as a character")
fn_expect_gid(t, f, 'C', 13, "likewise")
// The fallback is a last resort, not a licence: a code the cmap does not
// have is still nothing, and NOT a glyph index (the font has a cmap).
fn_expect_gid(t, f, 'D', 0, "no glyph, and not a glyph index either")
}
@(test)
test_font_cid_keyed_cff_as_simple_font :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A CID-keyed CFF embedded under a /Type1 dictionary. It has no glyph
// names and no encoding of its own (CFF 12), so the name route and the
// built-in-encoding route both find nothing; reading the code as a CID
// through the charset is the only thing that recovers the glyphs, and
// without it the page comes out blank.
prog := fn_cff(doc, []u16{65, 66, 67}, true)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Odd /Encoding /WinAnsiEncoding >>",
"<< /Flags 4 >>", "FontFile3", prog)
if f == nil { return }
testing.expect(t, f.cff != nil && f.cff.is_cid, "a CID-keyed CFF")
fn_expect_gid(t, f, 'A', 1, "code 65 is CID 65, which the charset puts at glyph 1")
fn_expect_gid(t, f, 'C', 3, "code 67 likewise")
fn_expect_gid(t, f, 'D', 0, "and a code the charset has no CID for")
}
@(test)
test_font_sfnt_under_a_type1c_subtype :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /FontFile3 with /Subtype /Type1C holding an sfnt-wrapped OpenType font.
// The dictionary is wrong and the four magic bytes are not: believing the
// subtype here would hand an sfnt to the CFF reader, which would reject
// it and lose a font that is perfectly readable.
cff := fn_cff(doc, []u16{34}, false)
uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1})
otf := fn_sfnt(doc, []Fn_Table{
{"head", fn_head(doc, 1000)},
{"maxp", fn_maxp(doc, 2)},
{"cmap", fn_cmap(doc, Fn_Cmap_Rec{3, 1, uni})},
{"CFF ", cff},
}, 0x4F54544F)
d := fn_dict(doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Mislabelled /Encoding /WinAnsiEncoding >>")
desc := fn_dict(doc, "<< /Flags 32 >>")
dict_set(desc, "FontFile3", cf_stream(doc, "<< /Subtype /Type1C >>", otf))
dict_set(d, "FontDescriptor", desc)
f := fn_load(t, doc, d)
if f == nil { return }
testing.expect(t, f.ttf != nil, "the sfnt wrapper should have parsed")
testing.expect(t, f.cff != nil, "and so should its CFF table")
testing.expect(t, !f.program_failed, "nothing failed here")
fn_expect_gid(t, f, 'A', 1, "and the glyph is reachable")
}
@(test)
test_font_missing_width_is_the_last_word :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /MissingWidth is consulted before the embedded program's own advances:
// a descriptor that carries one is the file being explicit about codes it
// did not list, and the program's hmtx is the guess made when it is not.
uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1})
prog := fn_ttf(doc, 3, 1000, []Fn_Cmap_Rec{{3, 1, uni}}, nil, []u16{0, 900, 900})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Explicit /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 /MissingWidth 42 >>", "FontFile2", prog)
if f == nil { return }
fn_expect_width(t, f, 'A', 42, "/MissingWidth beats the hmtx advance")
// Without one, the program's advance is what is left.
g := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Implicit /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile2", prog)
if g == nil { return }
fn_expect_width(t, g, 'A', 900, "and without one the hmtx advance is")
}
// ---------------------------------------------------------------------------
// Type1 programs
//
// A Type1 font is a PostScript header, the token `eexec`, and an enciphered
// Private dictionary whose /CharStrings are themselves enciphered. Both
// ciphers are four lines each, so the font below is assembled rather than
// pasted in as a byte literal: what is under test is the dictionary's use of
// the program's built-in encoding, and a literal would hide which byte was
// which.
// ---------------------------------------------------------------------------
// fn_t1_encrypt is the Type 1 cipher (Type 1 spec, 7.3), used with key 55665
// for the eexec block and 4330 for a charstring. `pad` random bytes are
// prefixed to the plaintext, which is what the decipherer drops.
@(private = "file")
fn_t1_encrypt :: proc(doc: ^Document, plain: []byte, key: u16, pad: int) -> []byte {
out := fn_alloc(doc, len(plain) + pad)
r := u32(key)
for i in 0 ..< len(out) {
// A padding byte that is not a hex digit keeps the eexec block from
// being mistaken for the hex-ASCII form a .pfa uses.
p := u8(0x7A) if i < pad else plain[i - pad]
c := p ~ u8(r >> 8)
out[i] = c
r = ((u32(c) + r) * 52845 + 22719) & 0xFFFF
}
return out
}
// FN_T1_CS is "0 500 hsbw 50 50 rmoveto closepath endchar": a glyph 500 units
// wide that draws a single point. Nine bytes, thirteen once the four bytes of
// /lenIV padding are enciphered in front of it.
@(private = "file")
FN_T1_CS := [?]u8{0x8B, 0xF8, 0x88, 0x0D, 0xBD, 0xBD, 0x15, 0x09, 0x0E}
// fn_type1 builds a font program with three glyphs -- .notdef, Aacute and
// beta -- and a built-in encoding putting the last two on codes 65 and 66.
// Its /FontMatrix is 2000 units per em rather than the usual 1000, so a
// caller that assumes thousandths gets visibly wrong widths.
@(private = "file")
fn_type1 :: proc(doc: ^Document) -> []byte {
cs := fn_t1_encrypt(doc, FN_T1_CS[:], 4330, 4)
priv := make([dynamic]u8, 0, 256, doc.allocator)
append_string(&priv, "dup /Private 8 dict dup begin\n/lenIV 4 def\nend\n")
append_string(&priv, "/CharStrings 3 dict dup begin\n")
for name in ([]string{"/.notdef ", "/Aacute ", "/beta "}) {
append_string(&priv, name, "13 RD ")
append(&priv, ..cs)
append_string(&priv, " ND\n")
}
append_string(&priv, "end\nend\n")
body := fn_t1_encrypt(doc, priv[:], 55665, 4)
out := make([dynamic]u8, 0, 512, doc.allocator)
append_string(&out,
"%!PS-AdobeFont-1.0: Test 001.001\n" +
"/FontName /Test def\n" +
"/FontMatrix [0.0005 0 0 0.0005 0 0] readonly def\n" +
"/Encoding 256 array\n" +
"0 1 255 {1 index exch /.notdef put} for\n" +
"dup 65 /Aacute put\n" +
"dup 66 /beta put\n" +
"readonly def\n" +
"currentdict end\n" +
"currentfile eexec ")
append(&out, ..body)
append_string(&out, "\n0000000000000000\ncleartomark\n")
return out[:]
}
@(test)
test_font_type1_builtin_encoding :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// With no /Encoding in the font dictionary, the program's own encoding is
// the encoding. A font that ignored it would put StandardEncoding's names
// on the codes and find neither of this font's two glyphs.
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Test >>",
"<< /Flags 4 >>", "FontFile", fn_type1(doc))
if f == nil { return }
testing.expect(t, f.t1 != nil, "the Type1 program should have parsed")
testing.expect(t, !f.program_failed && f.embedded, "and it is embedded")
testing.expect_value(t, f.enc_names[65], "Aacute")
testing.expect_value(t, f.enc_names[66], "beta")
fn_expect_gid(t, f, 65, 1, "Aacute is the second charstring")
fn_expect_gid(t, f, 66, 2, "beta is the third")
// 2000 units per em, and the program's own advances have to come back in
// the thousandths a PDF width is in: 500 of these units is a quarter em,
// which is 250 of those.
testing.expect_value(t, font_matrix(f).a, 0.0005)
fn_expect_width(t, f, 65, 250, "hsbw width through the FontMatrix")
p := path_make()
defer path_destroy(&p)
testing.expect_value(t, font_glyph_path(f, 1, &p), Error.None)
testing.expect(t, !path_is_empty(&p), "the charstring draws a point")
}
@(test)
test_font_type1_differences_over_builtin :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /Differences is applied over the program's built-in encoding, not over
// StandardEncoding: code 65 becomes beta, and code 66 keeps the built-in
// name the program gave it.
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Test /Encoding " +
"<< /Differences [65 /beta] >> >>",
"<< /Flags 4 >>", "FontFile", fn_type1(doc))
if f == nil { return }
testing.expect_value(t, f.enc_names[65], "beta")
testing.expect_value(t, f.enc_names[66], "beta")
fn_expect_gid(t, f, 65, 2, "/Differences moved beta onto code 65")
fn_expect_gid(t, f, 66, 2, "and the built-in encoding still names 66")
// A /BaseEncoding replaces the built-in one wholesale, and this font has
// none of StandardEncoding's names, so nothing resolves.
g := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Test " +
"/Encoding /WinAnsiEncoding >>",
"<< /Flags 4 >>", "FontFile", fn_type1(doc))
if g == nil { return }
testing.expect_value(t, g.enc_names[65], "A")
fn_expect_gid(t, g, 65, 0, "the font has no glyph called A")
}
// FN_GLYF_SQUARE is one contour of four on-curve points at (0,0) (100,0)
// (100,100) (0,100). The same 22 bytes font_truetype_test.odin builds its
// outlines from; here they exist only so that there is a real outline to
// route a font_glyph_path call to.
@(private = "file")
FN_GLYF_SQUARE := [?]u8{
0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64,
0x00, 0x03,
0x00, 0x00,
0x31, 0x33, 0x35, 0x21,
0x64, 0xFF, 0x9C,
0x64,
}
@(test)
test_font_glyph_path_truetype :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A font with real `glyf` outlines has to be drawn from them. The two
// readers a Font can hold are not interchangeable, and reaching for the
// wrong one draws nothing at all.
loca := fn_alloc(doc, 6)
fn_w16(loca, 0, 0)
fn_w16(loca, 2, 0)
fn_w16(loca, 4, u16(len(FN_GLYF_SQUARE) / 2)) // short loca stores halves
uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1})
prog := fn_sfnt(doc, []Fn_Table{
{"head", fn_head(doc, 1000)},
{"maxp", fn_maxp(doc, 2)},
{"loca", loca},
{"glyf", FN_GLYF_SQUARE[:]},
{"cmap", fn_cmap(doc, Fn_Cmap_Rec{3, 1, uni})},
})
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /TrueType /BaseFont /Sq /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile2", prog)
if f == nil { return }
fn_expect_gid(t, f, 'A', 1, "A is glyph 1")
p := path_make()
defer path_destroy(&p)
testing.expect_value(t, font_glyph_path(f, 1, &p), Error.None)
testing.expect(t, len(p.points) >= 4, "the square has four points")
testing.expect_value(t, p.points[0], Point{0, 0})
// Glyph space, untouched by the font matrix: 100 font units, not 0.1.
testing.expect_value(t, p.points[1], Point{100, 0})
// A glyph the font does not have draws nothing rather than failing.
path_clear(&p)
testing.expect_value(t, font_glyph_path(f, 9, &p), Error.No_Glyph)
}
@(test)
test_font_descendant_written_directly :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// 9.7.6.1 requires /DescendantFonts to be a one-element array. Producers
// write the dictionary itself, and it means the same thing: refusing it
// would cost every width and every glyph in the font.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"<< /Subtype /CIDFontType2 /DW 640 /W [3 3 900] >> >>")
if f == nil { return }
testing.expect(t, f.descendant != nil, "the descendant should have been found")
testing.expect_value(t, font_cid_width(f, 1), 640)
testing.expect_value(t, font_cid_width(f, 3), 900)
}
@(test)
test_font_cff_font_matrix_and_advance :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A CFF that declares its own FontMatrix. The 1/1000 default is what all
// but a handful of fonts use, which is exactly why the handful have to be
// honoured: at 2000 units per em, a font drawn through thousandths comes
// out twice too big, and the charstring's own advance -- which is in those
// same units -- has to be halved on its way into a PDF width.
prog := fn_cff(doc, []u16{34, 35}, false, true)
f := fn_simple(t, doc,
"<< /Type /Font /Subtype /Type1 /BaseFont /Wide /Encoding /WinAnsiEncoding >>",
"<< /Flags 32 >>", "FontFile3", prog)
if f == nil { return }
testing.expect(t, f.cff != nil, "the CFF should have parsed")
testing.expect_value(t, font_matrix(f).a, 0.0005)
testing.expect_value(t, font_matrix(f).d, 0.0005)
// The charstring declares 400 glyph-space units, which is a fifth of this
// font's em, which is 200 thousandths.
fn_expect_width(t, f, 'A', 200, "charstring advance through the FontMatrix")
}
@(test)
test_font_widths_array_clamped :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A /Widths array longer than the 256 codes that exist. The length came
// out of the file, so it is clamped rather than believed; the entries
// that can actually be indexed still have to be the right ones.
a := new_array(doc.allocator, 512)
for i in 0 ..< 400 {
array_push(a, i64(1000 + i))
}
d := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Long /FirstChar 0 >>")
dict_set(d, "Widths", a)
f := fn_load(t, doc, d)
if f == nil { return }
testing.expect_value(t, len(f.widths), 256)
fn_expect_width(t, f, 0, 1000, "the first entry")
fn_expect_width(t, f, 255, 1255, "the last code that exists")
}
@(test)
test_font_type0_w_out_of_order :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /W is not required to be sorted and files do write it backwards. The
// lookup is a binary search, so the entries have to be put in order first
// or every width past the first is wrong.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 5 /W [90 92 900 50 52 500 10 12 100] >>] >>")
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 11), 100)
testing.expect_value(t, font_cid_width(f, 51), 500)
testing.expect_value(t, font_cid_width(f, 91), 900)
testing.expect_value(t, font_cid_width(f, 60), 5)
testing.expect_value(t, font_cid_width(f, 9), 5)
testing.expect_value(t, font_cid_width(f, 93), 5)
}
@(test)
test_font_type0_w_broad_range_split :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A narrow correction written inside a broad run splits it in three: the
// part before, the correction, and the part after. Dropping the part after
// -- the easy mistake, since it is the half that is not adjacent to
// anything -- silently gives every CID above the correction the default
// width, which is most of the font.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 7 /W [1 100 300 50 50 900] >>] >>")
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 1), 300)
testing.expect_value(t, font_cid_width(f, 49), 300)
testing.expect_value(t, font_cid_width(f, 50), 900)
testing.expect_value(t, font_cid_width(f, 51), 300)
testing.expect_value(t, font_cid_width(f, 100), 300)
testing.expect_value(t, font_cid_width(f, 101), 7)
testing.expect_value(t, font_cid_width(f, 0), 7)
}
@(test)
test_font_type0_w_many_overrides :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// One run over the whole CID space with forty single-CID corrections
// written inside it -- a perfectly legal way to say "everything is 300
// except these". The answer for a CID above all forty is still 300, and
// it lies forty entries behind where a search by starting CID lands: any
// resolution that scans back a fixed distance from there gets it wrong.
a := new_array(doc.allocator, 256)
array_push(a, i64(0))
array_push(a, i64(2000))
array_push(a, i64(300))
for c in 1 ..= 40 {
array_push(a, i64(c))
array_push(a, i64(c))
array_push(a, i64(900))
}
cid := fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 5 >>")
dict_set(cid, "W", a)
f := fn_type0(t, doc, "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H >>", cid)
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 7), 900)
testing.expect_value(t, font_cid_width(f, 40), 900)
testing.expect_value(t, font_cid_width(f, 41), 300)
testing.expect_value(t, font_cid_width(f, 500), 300)
testing.expect_value(t, font_cid_width(f, 2000), 300)
testing.expect_value(t, font_cid_width(f, 2001), 5)
}
@(test)
test_font_type0_w2_out_of_order :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// The same treatment for /W2: written backwards, with a correction inside
// a broad run.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-V /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 500 /W2 [20 20 -700 200 750 1 100 -900 50 880] >>] >>")
if f == nil { return }
w, vx, _ := font_cid_vmetric(f, 20, 500)
testing.expect_value(t, w, -700)
testing.expect_value(t, vx, 200)
w, vx, _ = font_cid_vmetric(f, 19, 500)
testing.expect_value(t, w, -900)
testing.expect_value(t, vx, 50)
w, vx, _ = font_cid_vmetric(f, 21, 500)
testing.expect_value(t, w, -900)
w, _, _ = font_cid_vmetric(f, 101, 500)
testing.expect_value(t, w, -1000) // the /DW2 default
}
@(test)
test_font_type0_w_nested_overlaps :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Three runs nested inside one another, each narrower and later than the
// last: 0..100 is A, 10..20 is B written over it, and 11..12 is C written
// over that. Every CID has exactly one right answer --
//
// 0..9 A, 10 B, 11..12 C, 13..20 B, 21..100 A
//
// -- and the shape is what catches an implementation that keeps only one
// leftover piece per correction: the part of A above 20 is left stranded
// behind the part of B above 12, and both have to come back.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 7 /W [0 100 100 10 20 200 11 12 300] >>] >>")
if f == nil { return }
for pair in ([][2]f64{
{0, 100}, {9, 100}, {10, 200}, {11, 300}, {12, 300},
{13, 200}, {20, 200}, {21, 100}, {100, 100}, {101, 7},
}) {
got := font_cid_width(f, u32(pair[0]))
testing.expectf(t, got == pair[1], "CID %v is %v wide, wanted %v",
pair[0], got, pair[1])
}
}
@(test)
test_font_type0_w_identical_and_adjacent :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Two runs covering exactly the same CIDs, and a run that abuts another
// without overlapping it. Neither may lose a CID or invent one.
f := fn_font(t, doc,
"<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " +
"[<< /Subtype /CIDFontType2 /DW 7 /W [5 9 100 5 9 200 10 14 300] >>] >>")
if f == nil { return }
testing.expect_value(t, font_cid_width(f, 4), 7)
// One of the two identical runs wins; which is the file's problem, but a
// CID they both cover must not fall through to /DW.
w := font_cid_width(f, 7)
testing.expectf(t, w == 100 || w == 200, "CID 7 fell through to /DW: %v", w)
testing.expect_value(t, font_cid_width(f, 9), w)
testing.expect_value(t, font_cid_width(f, 10), 300)
testing.expect_value(t, font_cid_width(f, 14), 300)
testing.expect_value(t, font_cid_width(f, 15), 7)
}