Files
pdf-odin-llm/font_test.odin
2026-09-25 23:32:29 +02:00

2172 lines
86 KiB
Odin
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}