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> endcodespacerange 1 begincidrange <00> 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> 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) }