package pdf import "core:testing" // Every width asserted here was read out of the Adobe Core 14 AFM file named in // the comment above it, not remembered. A width that is wrong by one unit is // invisible on the first glyph of a line and unmissable by the last, so these // are spot checks against the source of truth rather than against this file's // own tables. @(test) test_std_font_widths_match_afm :: proc(t: ^testing.T) { cases := [?]struct { font: int, glyph: string, width: f64, }{ // Courier.afm {STD_COURIER, "space", 600}, {STD_COURIER, "A", 600}, {STD_COURIER, "W", 600}, {STD_COURIER, "a", 600}, {STD_COURIER, "m", 600}, {STD_COURIER, "one", 600}, {STD_COURIER, "comma", 600}, {STD_COURIER, "period", 600}, {STD_COURIER, "quoteright", 600}, {STD_COURIER, "quotesingle", 600}, {STD_COURIER, "fraction", 600}, {STD_COURIER, "germandbls", 600}, {STD_COURIER, "Euro", 600}, {STD_COURIER, "ydieresis", 600}, // Courier-Bold.afm {STD_COURIER_BOLD, "space", 600}, {STD_COURIER_BOLD, "A", 600}, {STD_COURIER_BOLD, "W", 600}, {STD_COURIER_BOLD, "a", 600}, {STD_COURIER_BOLD, "m", 600}, {STD_COURIER_BOLD, "one", 600}, {STD_COURIER_BOLD, "comma", 600}, {STD_COURIER_BOLD, "period", 600}, {STD_COURIER_BOLD, "quoteright", 600}, {STD_COURIER_BOLD, "quotesingle", 600}, {STD_COURIER_BOLD, "fraction", 600}, {STD_COURIER_BOLD, "germandbls", 600}, {STD_COURIER_BOLD, "Euro", 600}, {STD_COURIER_BOLD, "ydieresis", 600}, // Courier-Oblique.afm {STD_COURIER_OBLIQUE, "space", 600}, {STD_COURIER_OBLIQUE, "A", 600}, {STD_COURIER_OBLIQUE, "W", 600}, {STD_COURIER_OBLIQUE, "a", 600}, {STD_COURIER_OBLIQUE, "m", 600}, {STD_COURIER_OBLIQUE, "one", 600}, {STD_COURIER_OBLIQUE, "comma", 600}, {STD_COURIER_OBLIQUE, "period", 600}, {STD_COURIER_OBLIQUE, "quoteright", 600}, {STD_COURIER_OBLIQUE, "quotesingle", 600}, {STD_COURIER_OBLIQUE, "fraction", 600}, {STD_COURIER_OBLIQUE, "germandbls", 600}, {STD_COURIER_OBLIQUE, "Euro", 600}, {STD_COURIER_OBLIQUE, "ydieresis", 600}, // Courier-BoldOblique.afm {STD_COURIER_BOLD_OBLIQUE, "space", 600}, {STD_COURIER_BOLD_OBLIQUE, "A", 600}, {STD_COURIER_BOLD_OBLIQUE, "W", 600}, {STD_COURIER_BOLD_OBLIQUE, "a", 600}, {STD_COURIER_BOLD_OBLIQUE, "m", 600}, {STD_COURIER_BOLD_OBLIQUE, "one", 600}, {STD_COURIER_BOLD_OBLIQUE, "comma", 600}, {STD_COURIER_BOLD_OBLIQUE, "period", 600}, {STD_COURIER_BOLD_OBLIQUE, "quoteright", 600}, {STD_COURIER_BOLD_OBLIQUE, "quotesingle", 600}, {STD_COURIER_BOLD_OBLIQUE, "fraction", 600}, {STD_COURIER_BOLD_OBLIQUE, "germandbls", 600}, {STD_COURIER_BOLD_OBLIQUE, "Euro", 600}, {STD_COURIER_BOLD_OBLIQUE, "ydieresis", 600}, // Helvetica.afm {STD_HELVETICA, "space", 278}, {STD_HELVETICA, "A", 667}, {STD_HELVETICA, "W", 944}, {STD_HELVETICA, "a", 556}, {STD_HELVETICA, "m", 833}, {STD_HELVETICA, "one", 556}, {STD_HELVETICA, "comma", 278}, {STD_HELVETICA, "period", 278}, {STD_HELVETICA, "quoteright", 222}, {STD_HELVETICA, "quotesingle", 191}, {STD_HELVETICA, "fraction", 167}, {STD_HELVETICA, "germandbls", 611}, {STD_HELVETICA, "Euro", 556}, {STD_HELVETICA, "ydieresis", 500}, // Helvetica-Bold.afm {STD_HELVETICA_BOLD, "space", 278}, {STD_HELVETICA_BOLD, "A", 722}, {STD_HELVETICA_BOLD, "W", 944}, {STD_HELVETICA_BOLD, "a", 556}, {STD_HELVETICA_BOLD, "m", 889}, {STD_HELVETICA_BOLD, "one", 556}, {STD_HELVETICA_BOLD, "comma", 278}, {STD_HELVETICA_BOLD, "period", 278}, {STD_HELVETICA_BOLD, "quoteright", 278}, {STD_HELVETICA_BOLD, "quotesingle", 238}, {STD_HELVETICA_BOLD, "fraction", 167}, {STD_HELVETICA_BOLD, "germandbls", 611}, {STD_HELVETICA_BOLD, "Euro", 556}, {STD_HELVETICA_BOLD, "ydieresis", 556}, // Helvetica-Oblique.afm {STD_HELVETICA_OBLIQUE, "space", 278}, {STD_HELVETICA_OBLIQUE, "A", 667}, {STD_HELVETICA_OBLIQUE, "W", 944}, {STD_HELVETICA_OBLIQUE, "a", 556}, {STD_HELVETICA_OBLIQUE, "m", 833}, {STD_HELVETICA_OBLIQUE, "one", 556}, {STD_HELVETICA_OBLIQUE, "comma", 278}, {STD_HELVETICA_OBLIQUE, "period", 278}, {STD_HELVETICA_OBLIQUE, "quoteright", 222}, {STD_HELVETICA_OBLIQUE, "quotesingle", 191}, {STD_HELVETICA_OBLIQUE, "fraction", 167}, {STD_HELVETICA_OBLIQUE, "germandbls", 611}, {STD_HELVETICA_OBLIQUE, "Euro", 556}, {STD_HELVETICA_OBLIQUE, "ydieresis", 500}, // Helvetica-BoldOblique.afm {STD_HELVETICA_BOLD_OBLIQUE, "space", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "A", 722}, {STD_HELVETICA_BOLD_OBLIQUE, "W", 944}, {STD_HELVETICA_BOLD_OBLIQUE, "a", 556}, {STD_HELVETICA_BOLD_OBLIQUE, "m", 889}, {STD_HELVETICA_BOLD_OBLIQUE, "one", 556}, {STD_HELVETICA_BOLD_OBLIQUE, "comma", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "period", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "quoteright", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "quotesingle", 238}, {STD_HELVETICA_BOLD_OBLIQUE, "fraction", 167}, {STD_HELVETICA_BOLD_OBLIQUE, "germandbls", 611}, {STD_HELVETICA_BOLD_OBLIQUE, "Euro", 556}, {STD_HELVETICA_BOLD_OBLIQUE, "ydieresis", 556}, // Times-Roman.afm {STD_TIMES_ROMAN, "space", 250}, {STD_TIMES_ROMAN, "A", 722}, {STD_TIMES_ROMAN, "W", 944}, {STD_TIMES_ROMAN, "a", 444}, {STD_TIMES_ROMAN, "m", 778}, {STD_TIMES_ROMAN, "one", 500}, {STD_TIMES_ROMAN, "comma", 250}, {STD_TIMES_ROMAN, "period", 250}, {STD_TIMES_ROMAN, "quoteright", 333}, {STD_TIMES_ROMAN, "quotesingle", 180}, {STD_TIMES_ROMAN, "fraction", 167}, {STD_TIMES_ROMAN, "germandbls", 500}, {STD_TIMES_ROMAN, "Euro", 500}, {STD_TIMES_ROMAN, "ydieresis", 500}, // Times-Bold.afm {STD_TIMES_BOLD, "space", 250}, {STD_TIMES_BOLD, "A", 722}, {STD_TIMES_BOLD, "W", 1000}, {STD_TIMES_BOLD, "a", 500}, {STD_TIMES_BOLD, "m", 833}, {STD_TIMES_BOLD, "one", 500}, {STD_TIMES_BOLD, "comma", 250}, {STD_TIMES_BOLD, "period", 250}, {STD_TIMES_BOLD, "quoteright", 333}, {STD_TIMES_BOLD, "quotesingle", 278}, {STD_TIMES_BOLD, "fraction", 167}, {STD_TIMES_BOLD, "germandbls", 556}, {STD_TIMES_BOLD, "Euro", 500}, {STD_TIMES_BOLD, "ydieresis", 500}, // Times-Italic.afm {STD_TIMES_ITALIC, "space", 250}, {STD_TIMES_ITALIC, "A", 611}, {STD_TIMES_ITALIC, "W", 833}, {STD_TIMES_ITALIC, "a", 500}, {STD_TIMES_ITALIC, "m", 722}, {STD_TIMES_ITALIC, "one", 500}, {STD_TIMES_ITALIC, "comma", 250}, {STD_TIMES_ITALIC, "period", 250}, {STD_TIMES_ITALIC, "quoteright", 333}, {STD_TIMES_ITALIC, "quotesingle", 214}, {STD_TIMES_ITALIC, "fraction", 167}, {STD_TIMES_ITALIC, "germandbls", 500}, {STD_TIMES_ITALIC, "Euro", 500}, {STD_TIMES_ITALIC, "ydieresis", 444}, // Times-BoldItalic.afm {STD_TIMES_BOLD_ITALIC, "space", 250}, {STD_TIMES_BOLD_ITALIC, "A", 667}, {STD_TIMES_BOLD_ITALIC, "W", 889}, {STD_TIMES_BOLD_ITALIC, "a", 500}, {STD_TIMES_BOLD_ITALIC, "m", 778}, {STD_TIMES_BOLD_ITALIC, "one", 500}, {STD_TIMES_BOLD_ITALIC, "comma", 250}, {STD_TIMES_BOLD_ITALIC, "period", 250}, {STD_TIMES_BOLD_ITALIC, "quoteright", 333}, {STD_TIMES_BOLD_ITALIC, "quotesingle", 278}, {STD_TIMES_BOLD_ITALIC, "fraction", 167}, {STD_TIMES_BOLD_ITALIC, "germandbls", 500}, {STD_TIMES_BOLD_ITALIC, "Euro", 500}, {STD_TIMES_BOLD_ITALIC, "ydieresis", 444}, // Symbol.afm {STD_SYMBOL, "space", 250}, {STD_SYMBOL, "exclam", 333}, {STD_SYMBOL, "universal", 713}, {STD_SYMBOL, "numbersign", 500}, {STD_SYMBOL, "alpha", 631}, {STD_SYMBOL, "beta", 549}, {STD_SYMBOL, "gamma", 411}, {STD_SYMBOL, "Alpha", 722}, {STD_SYMBOL, "Omega", 768}, {STD_SYMBOL, "summation", 713}, {STD_SYMBOL, "integral", 274}, {STD_SYMBOL, "radical", 549}, {STD_SYMBOL, "infinity", 713}, {STD_SYMBOL, "copyrightserif", 790}, {STD_SYMBOL, "apple", 790}, // ZapfDingbats.afm {STD_ZAPF_DINGBATS, "space", 278}, {STD_ZAPF_DINGBATS, "a1", 974}, {STD_ZAPF_DINGBATS, "a5", 789}, {STD_ZAPF_DINGBATS, "a10", 692}, {STD_ZAPF_DINGBATS, "a60", 789}, {STD_ZAPF_DINGBATS, "a89", 390}, {STD_ZAPF_DINGBATS, "a100", 668}, {STD_ZAPF_DINGBATS, "a101", 732}, {STD_ZAPF_DINGBATS, "a109", 626}, {STD_ZAPF_DINGBATS, "a191", 918}, } for c in cases { w, ok := std_font_width(c.font, c.glyph) testing.expectf(t, ok, "%s has no width for %q", std_font_name(c.font), c.glyph) testing.expectf(t, w == c.width, "%s %q: width %v, want %v", std_font_name(c.font), c.glyph, w, c.width) } } // Courier is fixed pitch, so the width question has one answer for every name, // including names the font cannot draw: a viewer still moves the pen one cell. @(test) test_std_font_width_courier_is_fixed_pitch :: proc(t: ^testing.T) { for f in ([?]int{STD_COURIER, STD_COURIER_BOLD, STD_COURIER_OBLIQUE, STD_COURIER_BOLD_OBLIQUE}) { for name in ([?]string{"A", "m", "space", "Aacute", "a1", "uni4E00", "notaglyph"}) { w, ok := std_font_width(f, name) testing.expectf(t, ok && w == 600, "%s %q: %v %v", std_font_name(f), name, w, ok) } w, ok := std_font_width(f, "") testing.expect(t, !ok && w == 0, "an empty glyph name has no width even in Courier") } } // A name the font does not have must report ok = false rather than 0, because // the caller has to fall back to /MissingWidth and cannot tell those apart if // the width alone is returned. @(test) test_std_font_width_absent_and_out_of_range :: proc(t: ^testing.T) { for f in ([?]int{STD_HELVETICA, STD_TIMES_BOLD_ITALIC, STD_SYMBOL, STD_ZAPF_DINGBATS}) { for name in ([?]string{"", "notaglyph", "Asmall", "zzzzzz", "\x00"}) { w, ok := std_font_width(f, name) testing.expectf(t, !ok && w == 0, "%s %q: %v %v", std_font_name(f), name, w, ok) } } // Symbol and ZapfDingbats share no glyph names with the text fonts. _, ok := std_font_width(STD_HELVETICA, "alpha") testing.expect(t, !ok, "Helvetica has no alpha") _, ok = std_font_width(STD_SYMBOL, "A") testing.expect(t, !ok, "Symbol has no A -- code 0x41 is Alpha") _, ok = std_font_width(STD_ZAPF_DINGBATS, "A") testing.expect(t, !ok, "ZapfDingbats has no A") for f in ([?]int{-1, -1000, STD_FONT_COUNT, 99} ) { w, iok := std_font_width(f, "A") testing.expectf(t, !iok && w == 0, "index %d must not resolve", f) testing.expect(t, std_font_name(f) == "", "no name for a bad index") testing.expect(t, !std_font_is_symbolic(f), "a bad index is not symbolic") testing.expect(t, std_font_builtin_encoding(f) == nil, "no encoding for a bad index") } } @(test) test_std_font_lookup_canonical_names :: proc(t: ^testing.T) { for i in 0 ..< STD_FONT_COUNT { idx, ok := std_font_lookup(std_font_name(i)) testing.expectf(t, ok, "%q did not resolve", std_font_name(i)) testing.expectf(t, idx == i, "%q resolved to %d (%s), want %d", std_font_name(i), idx, std_font_name(idx), i) } } @(test) test_std_font_lookup_aliases :: proc(t: ^testing.T) { cases := [?]struct { name: string, want: int, }{ // The Monotype spellings Word and Acrobat emit. {"Arial", STD_HELVETICA}, {"ArialMT", STD_HELVETICA}, {"Arial-BoldMT", STD_HELVETICA_BOLD}, {"Arial-ItalicMT", STD_HELVETICA_OBLIQUE}, {"Arial-BoldItalicMT", STD_HELVETICA_BOLD_OBLIQUE}, {"Arial,Bold", STD_HELVETICA_BOLD}, {"Arial,BoldItalic", STD_HELVETICA_BOLD_OBLIQUE}, {"Arial-Black", STD_HELVETICA_BOLD}, {"ArialNarrow", STD_HELVETICA}, {"Helvetica-Italic", STD_HELVETICA_OBLIQUE}, {"Helvetica Bold Oblique", STD_HELVETICA_BOLD_OBLIQUE}, {"NimbusSanL-Regu", STD_HELVETICA}, {"NimbusSans-BoldItalic", STD_HELVETICA_BOLD_OBLIQUE}, {"TimesNewRoman", STD_TIMES_ROMAN}, {"TimesNewRomanPSMT", STD_TIMES_ROMAN}, {"TimesNewRomanPS-BoldMT", STD_TIMES_BOLD}, {"TimesNewRomanPS-ItalicMT", STD_TIMES_ITALIC}, {"TimesNewRomanPS-BoldItalicMT", STD_TIMES_BOLD_ITALIC}, {"Times", STD_TIMES_ROMAN}, {"Times New Roman,Bold", STD_TIMES_BOLD}, {"NimbusRomNo9L-Medi", STD_TIMES_ROMAN}, {"CourierNew", STD_COURIER}, {"CourierNewPSMT", STD_COURIER}, {"CourierNewPS-BoldMT", STD_COURIER_BOLD}, {"CourierNew,BoldItalic", STD_COURIER_BOLD_OBLIQUE}, {"CourierStd-Oblique", STD_COURIER_OBLIQUE}, {"NimbusMonL-Regu", STD_COURIER}, // The two symbolic fonts have no styles; a style in the name is noise. {"Symbol", STD_SYMBOL}, {"SymbolMT", STD_SYMBOL}, {"Symbol,Bold", STD_SYMBOL}, {"ZapfDingbats", STD_ZAPF_DINGBATS}, {"Dingbats", STD_ZAPF_DINGBATS}, {"ZapfDingbatsITC", STD_ZAPF_DINGBATS}, } for c in cases { idx, ok := std_font_lookup(c.name) testing.expectf(t, ok, "%q did not resolve", c.name) testing.expectf(t, idx == c.want, "%q resolved to %d (%s), want %d (%s)", c.name, idx, std_font_name(idx), c.want, std_font_name(c.want)) } } @(test) test_std_font_lookup_subset_prefix :: proc(t: ^testing.T) { cases := [?]struct { name: string, want: int, }{ {"ABCDEF+Helvetica", STD_HELVETICA}, {"AAAAAA+Arial-BoldMT", STD_HELVETICA_BOLD}, {"XYZabc+Times-Italic", STD_TIMES_ITALIC}, {"ABCD+Courier", STD_COURIER}, {"ABCDEF+ZapfDingbats", STD_ZAPF_DINGBATS}, } for c in cases { idx, ok := std_font_lookup(c.name) testing.expectf(t, ok && idx == c.want, "%q -> %d %v", c.name, idx, ok) } // The tag really is removed, not just ignored: a tag that spells a family // must not decide the family of the font behind it. idx, ok := std_font_lookup("TIMESX+Helvetica") testing.expect(t, ok && idx == STD_HELVETICA, "the subset tag must not be matched against") idx, ok = std_font_lookup("BOLDXX+Helvetica") testing.expect(t, ok && idx == STD_HELVETICA, "a tag spelling a style must not set the style") } @(test) test_std_font_lookup_rejects_other_fonts :: proc(t: ^testing.T) { for name in ([?]string{ "", "+", "ABCDEF+", "Garamond", "Wingdings", "Wingdings-Regular", "MS-Mincho", "Calibri", "Cambria-Bold", "DejaVuSans", "Verdana", "Georgia", "TrebuchetMS", "Webdings", "\x00\x01\x02", }) { idx, ok := std_font_lookup(name) testing.expectf(t, !ok && idx == 0, "%q must not resolve to a standard font", name) } // A name longer than the normalising buffer must be handled, not crash. long := make([]u8, 4096) defer delete(long) for i in 0 ..< len(long) { long[i] = 'x' } _, ok := std_font_lookup(string(long)) testing.expect(t, !ok, "a 4 KB name of nothing is not a standard font") } @(test) test_std_font_is_symbolic :: proc(t: ^testing.T) { for i in 0 ..< STD_FONT_COUNT { want := i == STD_SYMBOL || i == STD_ZAPF_DINGBATS testing.expectf(t, std_font_is_symbolic(i) == want, "%s", std_font_name(i)) } testing.expect(t, std_font_builtin_encoding(STD_SYMBOL) == encoding_symbol(), "") testing.expect(t, std_font_builtin_encoding(STD_ZAPF_DINGBATS) == encoding_zapf(), "") testing.expect(t, std_font_builtin_encoding(STD_HELVETICA) == encoding_standard(), "") testing.expect(t, std_font_builtin_encoding(STD_TIMES_ROMAN) == encoding_standard(), "") } // --------------------------------------------------------------------------- // Encodings // --------------------------------------------------------------------------- // The classic wrong-apostrophe bug: code 0x27 is a right single quote in // StandardEncoding and a straight typewriter apostrophe in WinAnsi, and the // straight one lives at 0xA9 in Standard while the curly one lives at 0x92 in // WinAnsi. Reading a WinAnsi document with the Standard table turns every // apostrophe into a curly quote and every 0x92 into nothing. @(test) test_encoding_quote_positions :: proc(t: ^testing.T) { std := encoding_standard() win := encoding_winansi() testing.expect_value(t, std[0x27], "quoteright") testing.expect_value(t, win[0x27], "quotesingle") testing.expect_value(t, std[0xA9], "quotesingle") testing.expect_value(t, win[0x92], "quoteright") testing.expect_value(t, std[0x60], "quoteleft") testing.expect_value(t, win[0x60], "grave") testing.expect_value(t, win[0x91], "quoteleft") // Standard has no code at all where WinAnsi keeps the Latin-1 letters. testing.expect_value(t, std[0xFC], "") testing.expect_value(t, win[0xFC], "udieresis") testing.expect_value(t, glyph_name_to_rune(std[0x27]), rune(0x2019)) testing.expect_value(t, glyph_name_to_rune(win[0x27]), rune(0x0027)) } // 0x80..0x9F is where WinAnsi stops being Latin-1: CP1252 puts the typographic // characters in the C1 control area, and the annex fills every code CP1252 // leaves undefined with bullet (ISO 32000-2, Annex D.2, note 3). @(test) test_encoding_winansi_c1_range :: proc(t: ^testing.T) { win := encoding_winansi() cases := [?]struct { code: int, name: string, }{ {0x80, "Euro"}, {0x81, "bullet"}, {0x82, "quotesinglbase"}, {0x83, "florin"}, {0x84, "quotedblbase"}, {0x85, "ellipsis"}, {0x86, "dagger"}, {0x87, "daggerdbl"}, {0x88, "circumflex"}, {0x89, "perthousand"}, {0x8A, "Scaron"}, {0x8B, "guilsinglleft"}, {0x8C, "OE"}, {0x8D, "bullet"}, {0x8E, "Zcaron"}, {0x8F, "bullet"}, {0x90, "bullet"}, {0x91, "quoteleft"}, {0x92, "quoteright"}, {0x93, "quotedblleft"}, {0x94, "quotedblright"}, {0x95, "bullet"}, {0x96, "endash"}, {0x97, "emdash"}, {0x98, "tilde"}, {0x99, "trademark"}, {0x9A, "scaron"}, {0x9B, "guilsinglright"}, {0x9C, "oe"}, {0x9D, "bullet"}, {0x9E, "zcaron"}, {0x9F, "Ydieresis"}, } for c in cases { testing.expectf(t, win[c.code] == c.name, "WinAnsi 0x%02X: %q, want %q", c.code, win[c.code], c.name) } // Latin-1 would have left all of these undefined. std := encoding_standard() for c in cases { testing.expectf(t, std[c.code] == "", "StandardEncoding 0x%02X is undefined", c.code) } } // Above 0xA0 WinAnsi is Latin-1 exactly, with two codes that carry a character // PDF draws as its plain form: a no-break space and a soft hyphen. @(test) test_encoding_winansi_is_latin1_above_a0 :: proc(t: ^testing.T) { win := encoding_winansi() for code in 0xA0 ..= 0xFF { name := win[code] testing.expectf(t, name != "", "WinAnsi 0x%02X is defined", code) want := rune(code) switch code { case 0xA0: want = 0x0020 // no-break space, drawn as a space case 0xAD: want = 0x002D // soft hyphen, drawn as a hyphen } testing.expectf(t, glyph_name_to_rune(name) == want, "WinAnsi 0x%02X (%s) -> U+%04X, want U+%04X", code, name, glyph_name_to_rune(name), want) } // ASCII is shared by both Latin encodings apart from the two quote codes. std := encoding_standard() for code in 0x20 ..= 0x7E { if code == 0x27 || code == 0x60 { continue } testing.expectf(t, std[code] == win[code], "0x%02X: %q vs %q", code, std[code], win[code]) testing.expectf(t, glyph_name_to_rune(win[code]) == rune(code), "WinAnsi 0x%02X (%s)", code, win[code]) } } @(test) test_encoding_macroman_high_range :: proc(t: ^testing.T) { mac := encoding_macroman() cases := [?]struct { code: int, name: string, }{ {0x80, "Adieresis"}, {0x81, "Aring"}, {0x82, "Ccedilla"}, {0x83, "Eacute"}, {0x84, "Ntilde"}, {0x85, "Odieresis"}, {0x86, "Udieresis"}, {0x87, "aacute"}, {0x88, "agrave"}, {0x89, "acircumflex"}, {0x8A, "adieresis"}, {0x8B, "atilde"}, {0x8C, "aring"}, {0x8D, "ccedilla"}, {0x8E, "eacute"}, {0x8F, "egrave"}, {0x90, "ecircumflex"}, {0x91, "edieresis"}, {0x92, "iacute"}, {0x93, "igrave"}, {0x94, "icircumflex"}, {0x95, "idieresis"}, {0x96, "ntilde"}, {0x97, "oacute"}, {0x98, "ograve"}, {0x99, "ocircumflex"}, {0x9A, "odieresis"}, {0x9B, "otilde"}, {0x9C, "uacute"}, {0x9D, "ugrave"}, {0x9E, "ucircumflex"}, {0x9F, "udieresis"}, {0xA0, "dagger"}, {0xA1, "degree"}, {0xA2, "cent"}, {0xA3, "sterling"}, {0xA4, "section"}, {0xA5, "bullet"}, {0xA6, "paragraph"}, {0xA7, "germandbls"}, {0xA8, "registered"}, {0xA9, "copyright"}, {0xAA, "trademark"}, {0xAB, "acute"}, {0xAC, "dieresis"}, {0xAD, "notequal"}, {0xAE, "AE"}, {0xAF, "Oslash"}, {0xB0, "infinity"}, {0xB1, "plusminus"}, {0xB2, "lessequal"}, {0xB3, "greaterequal"}, {0xB4, "yen"}, {0xB5, "mu"}, {0xB6, "partialdiff"}, {0xB7, "summation"}, {0xB8, "product"}, {0xB9, "pi"}, {0xBA, "integral"}, {0xBB, "ordfeminine"}, {0xBC, "ordmasculine"}, {0xBD, "Omega"}, {0xBE, "ae"}, {0xBF, "oslash"}, {0xC0, "questiondown"}, {0xC1, "exclamdown"}, {0xC2, "logicalnot"}, {0xC3, "radical"}, {0xC4, "florin"}, {0xC5, "approxequal"}, {0xC6, "Delta"}, {0xC7, "guillemotleft"}, {0xC8, "guillemotright"}, {0xC9, "ellipsis"}, {0xCA, "space"}, {0xCB, "Agrave"}, {0xCC, "Atilde"}, {0xCD, "Otilde"}, {0xCE, "OE"}, {0xCF, "oe"}, {0xD0, "endash"}, {0xD1, "emdash"}, {0xD2, "quotedblleft"}, {0xD3, "quotedblright"}, {0xD4, "quoteleft"}, {0xD5, "quoteright"}, {0xD6, "divide"}, {0xD7, "lozenge"}, {0xD8, "ydieresis"}, {0xD9, "Ydieresis"}, {0xDA, "fraction"}, {0xDB, "currency"}, {0xDC, "guilsinglleft"}, {0xDD, "guilsinglright"}, {0xDE, "fi"}, {0xDF, "fl"}, {0xE0, "daggerdbl"}, {0xE1, "periodcentered"}, {0xE2, "quotesinglbase"}, {0xE3, "quotedblbase"}, {0xE4, "perthousand"}, {0xE5, "Acircumflex"}, {0xE6, "Ecircumflex"}, {0xE7, "Aacute"}, {0xE8, "Edieresis"}, {0xE9, "Egrave"}, {0xEA, "Iacute"}, {0xEB, "Icircumflex"}, {0xEC, "Idieresis"}, {0xED, "Igrave"}, {0xEE, "Oacute"}, {0xEF, "Ocircumflex"}, {0xF0, "apple"}, {0xF1, "Ograve"}, {0xF2, "Uacute"}, {0xF3, "Ucircumflex"}, {0xF4, "Ugrave"}, {0xF5, "dotlessi"}, {0xF6, "circumflex"}, {0xF7, "tilde"}, {0xF8, "macron"}, {0xF9, "breve"}, {0xFA, "dotaccent"}, {0xFB, "ring"}, {0xFC, "cedilla"}, {0xFD, "hungarumlaut"}, {0xFE, "ogonek"}, {0xFF, "caron"}, } for c in cases { testing.expectf(t, mac[c.code] == c.name, "MacRoman 0x%02X: %q, want %q", c.code, mac[c.code], c.name) } // 0xCA is Mac OS Roman's no-break space; PDF draws it as an ordinary space. testing.expect_value(t, mac[0xCA], "space") // MacRoman is not Latin-1: 0x80 is A-dieresis, not a control code, and the // accented letters are in a completely different order. win := encoding_winansi() differing := 0 for code in 0x80 ..= 0xFF { if mac[code] != win[code] { differing += 1 } } testing.expectf(t, differing > 100, "MacRoman and WinAnsi differ widely above 0x80, got %d", differing) } // ISO 32000-2 Annex D.2 leaves these fifteen positions blank, but Mac OS Roman // defines them and producers emitted them; every other reader fills them in. @(test) test_encoding_macroman_mac_os_extras :: proc(t: ^testing.T) { mac := encoding_macroman() cases := [?]struct { code: int, name: string, cp: rune, }{ {0xAD, "notequal", 0x2260}, {0xB0, "infinity", 0x221E}, {0xB2, "lessequal", 0x2264}, {0xB3, "greaterequal", 0x2265}, {0xB6, "partialdiff", 0x2202}, {0xB7, "summation", 0x2211}, {0xB8, "product", 0x220F}, {0xB9, "pi", 0x03C0}, {0xBA, "integral", 0x222B}, {0xBD, "Omega", 0x2126}, {0xC3, "radical", 0x221A}, {0xC5, "approxequal", 0x2248}, {0xC6, "Delta", 0x2206}, {0xD7, "lozenge", 0x25CA}, {0xF0, "apple", 0xF8FF}, } for c in cases { testing.expectf(t, mac[c.code] == c.name, "MacRoman 0x%02X: %q", c.code, mac[c.code]) testing.expectf(t, glyph_name_to_rune(c.name) == c.cp, "%s -> U+%04X, want U+%04X", c.name, glyph_name_to_rune(c.name), c.cp) } } @(test) test_encoding_standard_high_range :: proc(t: ^testing.T) { std := encoding_standard() cases := [?]struct { code: int, name: string, }{ {0xA1, "exclamdown"}, {0xA2, "cent"}, {0xA3, "sterling"}, {0xA4, "fraction"}, {0xA5, "yen"}, {0xA6, "florin"}, {0xA7, "section"}, {0xA8, "currency"}, {0xA9, "quotesingle"}, {0xAA, "quotedblleft"}, {0xAB, "guillemotleft"}, {0xAC, "guilsinglleft"}, {0xAD, "guilsinglright"}, {0xAE, "fi"}, {0xAF, "fl"}, {0xB1, "endash"}, {0xB2, "dagger"}, {0xB3, "daggerdbl"}, {0xB4, "periodcentered"}, {0xB6, "paragraph"}, {0xB7, "bullet"}, {0xB8, "quotesinglbase"}, {0xB9, "quotedblbase"}, {0xBA, "quotedblright"}, {0xBB, "guillemotright"}, {0xBC, "ellipsis"}, {0xBD, "perthousand"}, {0xBF, "questiondown"}, {0xC1, "grave"}, {0xC2, "acute"}, {0xC3, "circumflex"}, {0xC4, "tilde"}, {0xC5, "macron"}, {0xC6, "breve"}, {0xC7, "dotaccent"}, {0xC8, "dieresis"}, {0xCA, "ring"}, {0xCB, "cedilla"}, {0xCD, "hungarumlaut"}, {0xCE, "ogonek"}, {0xCF, "caron"}, {0xD0, "emdash"}, {0xE1, "AE"}, {0xE3, "ordfeminine"}, {0xE8, "Lslash"}, {0xE9, "Oslash"}, {0xEA, "OE"}, {0xEB, "ordmasculine"}, {0xF1, "ae"}, {0xF5, "dotlessi"}, {0xF8, "lslash"}, {0xF9, "oslash"}, {0xFA, "oe"}, {0xFB, "germandbls"}, } seen: [256]bool for c in cases { testing.expectf(t, std[c.code] == c.name, "Standard 0x%02X: %q, want %q", c.code, std[c.code], c.name) seen[c.code] = true } // Everything above 0x7F that the table above does not name is undefined. for code in 0x80 ..= 0xFF { if !seen[code] { testing.expectf(t, std[code] == "", "Standard 0x%02X should be undefined, got %q", code, std[code]) } } // StandardEncoding has no accented letters at all; that is why a document // in French needs WinAnsi, MacRoman or a /Differences array. for name in ([?]string{"eacute", "ccedilla", "udieresis", "Adieresis"}) { found := false for code in 0 ..< 256 { if std[code] == name { found = true } } testing.expectf(t, !found, "StandardEncoding must not contain %s", name) } } @(test) test_encoding_symbol_and_zapf :: proc(t: ^testing.T) { sym := encoding_symbol() testing.expect_value(t, sym[0x20], "space") testing.expect_value(t, sym[0x41], "Alpha") testing.expect_value(t, sym[0x61], "alpha") testing.expect_value(t, sym[0x62], "beta") testing.expect_value(t, sym[0xD6], "radical") testing.expect_value(t, sym[0xE5], "summation") testing.expect_value(t, sym[0xF2], "integral") testing.expect_value(t, sym[0xA0], "Euro") // Symbol's 0x41 is not "A": reading it with a Latin table gives Greek text // spelled in Latin letters, the usual symptom of a missing built-in encoding. testing.expect(t, sym[0x41] != encoding_standard()[0x41], "") zapf := encoding_zapf() testing.expect_value(t, zapf[0x20], "space") testing.expect_value(t, zapf[0x21], "a1") testing.expect_value(t, zapf[0x41], "a10") testing.expect_value(t, zapf[0x61], "a60") testing.expect_value(t, zapf[0x7E], "a100") testing.expect_value(t, zapf[0x80], "a89") testing.expect_value(t, zapf[0xA1], "a101") testing.expect_value(t, zapf[0xFE], "a191") testing.expect_value(t, zapf[0xFF], "") exp := encoding_macexpert() testing.expect_value(t, exp[0x20], "space") testing.expect_value(t, exp[0x21], "exclamsmall") testing.expect_value(t, exp[0x30], "zerooldstyle") testing.expect_value(t, exp[0x61], "Asmall") testing.expect_value(t, exp[0xF6], "nsuperior") testing.expect_value(t, exp[0x40], "") } @(test) test_encoding_by_name :: proc(t: ^testing.T) { testing.expect(t, encoding_by_name("WinAnsiEncoding") == encoding_winansi(), "") testing.expect(t, encoding_by_name("MacRomanEncoding") == encoding_macroman(), "") testing.expect(t, encoding_by_name("MacExpertEncoding") == encoding_macexpert(), "") testing.expect(t, encoding_by_name("StandardEncoding") == encoding_standard(), "") testing.expect(t, encoding_by_name("WinAnsi") == encoding_winansi(), "") testing.expect(t, encoding_by_name("MacRoman") == encoding_macroman(), "") // An unknown name is nil, which the caller must read as "leave the font's // built-in encoding alone" rather than "reset to Standard". for name in ([?]string{"", "Encoding", "winansiencoding", "WinAnsiEncodings", "PDFDocEncoding", "Identity-H", "\x00"}) { testing.expectf(t, encoding_by_name(name) == nil, "%q must not resolve", name) } // The four tables are genuinely different objects. testing.expect(t, encoding_standard() != encoding_winansi(), "") testing.expect(t, encoding_winansi() != encoding_macroman(), "") testing.expect(t, encoding_macroman() != encoding_macexpert(), "") } // --------------------------------------------------------------------------- // Glyph names // --------------------------------------------------------------------------- @(test) test_glyph_name_to_rune_agl_algorithm :: proc(t: ^testing.T) { cases := [?]struct { name: string, want: rune, }{ // Ordinary AGL names. {"A", 0x0041}, {"space", 0x0020}, {"quoteright", 0x2019}, {"eacute", 0x00E9}, {"germandbls", 0x00DF}, {"fi", 0xFB01}, {"Alpha", 0x0391}, {"alpha", 0x03B1}, {"afii10017", 0x0410}, // Cyrillic capital A {"Acyrillic", 0x0410}, {"summation", 0x2211}, {"a1", 0x2701}, // ZapfDingbats' own list {"Euro", 0x20AC}, // uniXXXX, one code point per four digits; the first one wins. {"uni0041", 0x0041}, {"uni00e9", 0x00E9}, // lowercase hex is not AGL-legal but common {"uni20AC", 0x20AC}, {"uni00410042", 0x0041}, // uXXXX through uXXXXXX. {"u0041", 0x0041}, {"u1F600", 0x1F600}, {"u10FFFF", 0x10FFFF}, // A suffix after a period is a stylistic variant of the same character. {"a.sc", 0x0061}, {"one.oldstyle", 0x0031}, {"A.alt01", 0x0041}, {"uni0041.swash", 0x0041}, // An underscore joins components; a ligature is named by its parts and // only the first can be represented as one rune. {"f_i", 0x0066}, {"f_f_l", 0x0066}, {"uni0066_uni0069", 0x0066}, // The AGL is consulted before the uni/u forms, or a real name that // begins with those letters would be parsed as hex. {"union", 0x222A}, {"universal", 0x2200}, {"uacute", 0x00FA}, {"u", 0x0075}, {"g", 0x0067}, // Nothing at all. {"", -1}, {".notdef", -1}, {".null", -1}, {"_", -1}, {"g3", -1}, // a glyph index, not a character {"g127", -1}, {"cid42", -1}, {"index7", -1}, {"glyph12", -1}, {"uni", -1}, {"uni41", -1}, // not a multiple of four digits {"uni00411", -1}, {"uniZZZZ", -1}, {"uniD800", -1}, // a lone surrogate is not a character {"uniDFFF", -1}, {"uni0041D800", -1}, // and not in a later group either {"uni004100", -1}, // a trailing part-group is not a name {"u110000", -1}, // past the end of the code space {"uD800", -1}, {"u041", -1}, // fewer than four digits {"u00000041", -1}, // more than six {"notaglyphname", -1}, {"\x00\x01", -1}, } for c in cases { got := glyph_name_to_rune(c.name) testing.expectf(t, got == c.want, "%q -> U+%04X (%d), want U+%04X (%d)", c.name, got, got, c.want, c.want) } } // The ASCII range must survive a trip through both directions, because that is // the path every /Differences array and every text extraction takes. @(test) test_rune_to_glyph_name_ascii_round_trip :: proc(t: ^testing.T) { for r in rune(0x20) ..= rune(0x7E) { name := rune_to_glyph_name(r) testing.expectf(t, name != "", "U+%04X has no glyph name", r) back := glyph_name_to_rune(name) testing.expectf(t, back == r, "U+%04X -> %q -> U+%04X", r, name, back) } // The names are the plain ones, not an obscure synonym. testing.expect_value(t, rune_to_glyph_name(' '), "space") testing.expect_value(t, rune_to_glyph_name('A'), "A") testing.expect_value(t, rune_to_glyph_name('z'), "z") testing.expect_value(t, rune_to_glyph_name('0'), "zero") testing.expect_value(t, rune_to_glyph_name('.'), "period") testing.expect_value(t, rune_to_glyph_name('-'), "hyphen") } @(test) test_rune_to_glyph_name_beyond_ascii :: proc(t: ^testing.T) { cases := [?]struct { r: rune, name: string, }{ {0x00E9, "eacute"}, {0x00DF, "germandbls"}, {0x2019, "quoteright"}, {0x2014, "emdash"}, {0x20AC, "Euro"}, {0x0391, "Alpha"}, {0x03C0, "pi"}, {0xFB01, "fi"}, {0x2701, "a1"}, } for c in cases { testing.expectf(t, rune_to_glyph_name(c.r) == c.name, "U+%04X -> %q, want %q", c.r, rune_to_glyph_name(c.r), c.name) testing.expectf(t, glyph_name_to_rune(c.name) == c.r, "%q -> U+%04X", c.name, glyph_name_to_rune(c.name)) } // Code points the list does not name come back empty rather than as a // fabricated "uniXXXX", which a font would not find either. for r in ([?]rune{-1, 0, 0x0378, 0x4E00, 0x1F600, 0x10FFFF, 0x110000}) { testing.expectf(t, rune_to_glyph_name(r) == "", "U+%04X must have no name", r) } } // --------------------------------------------------------------------------- // Table invariants // --------------------------------------------------------------------------- // Every lookup here is a binary search, so a table that is out of order fails // silently for some of its entries and not others. Walking each table proves // both the ordering and that the search finds everything in it. @(test) test_tables_are_sorted_and_complete :: proc(t: ^testing.T) { for i in 1 ..< len(STD_TEXT_GLYPHS) { testing.expectf(t, STD_TEXT_GLYPHS[i-1] < STD_TEXT_GLYPHS[i], "STD_TEXT_GLYPHS out of order at %d: %q then %q", i, STD_TEXT_GLYPHS[i-1], STD_TEXT_GLYPHS[i]) } for i in 1 ..< len(STD_SYMBOL_GLYPHS) { testing.expectf(t, STD_SYMBOL_GLYPHS[i-1] < STD_SYMBOL_GLYPHS[i], "STD_SYMBOL_GLYPHS at %d", i) } for i in 1 ..< len(STD_ZAPF_GLYPHS) { testing.expectf(t, STD_ZAPF_GLYPHS[i-1] < STD_ZAPF_GLYPHS[i], "STD_ZAPF_GLYPHS at %d", i) } for i in 1 ..< len(AGL_TABLE) { testing.expectf(t, AGL_TABLE[i-1].name < AGL_TABLE[i].name, "AGL_TABLE out of order at %d: %q then %q", i, AGL_TABLE[i-1].name, AGL_TABLE[i].name) } for i in 1 ..< len(AGL_BY_RUNE) { testing.expectf(t, AGL_TABLE[AGL_BY_RUNE[i-1]].cp < AGL_TABLE[AGL_BY_RUNE[i]].cp, "AGL_BY_RUNE out of order at %d", i) } // Every name is findable, and every proportional width is plausible: the // narrowest glyph in any of the fourteen is 138 units and the widest 1042. for name, i in STD_TEXT_GLYPHS { testing.expectf(t, len(name) > 0, "empty name at %d", i) for f in STD_HELVETICA ..= STD_TIMES_BOLD_ITALIC { w, ok := std_font_width(f, name) testing.expectf(t, ok, "%s has no %q", std_font_name(f), name) testing.expectf(t, w >= 138 && w <= 1042, "%s %q: %v", std_font_name(f), name, w) } } for name in STD_SYMBOL_GLYPHS { w, ok := std_font_width(STD_SYMBOL, name) testing.expectf(t, ok && w >= 138 && w <= 1042, "Symbol %q: %v %v", name, w, ok) } for name in STD_ZAPF_GLYPHS { w, ok := std_font_width(STD_ZAPF_DINGBATS, name) testing.expectf(t, ok && w >= 138 && w <= 1042, "ZapfDingbats %q: %v %v", name, w, ok) } // agl_is_index_name is only equivalent to the fallthrough while the table // holds no name shaped like a glyph index; that is the invariant it rests on. for e in AGL_TABLE { for p in ([?]string{"g", "cid", "index", "glyph"}) { if len(e.name) <= len(p) || e.name[:len(p)] != p { continue } digits := true for j in len(p) ..< len(e.name) { if e.name[j] < '0' || e.name[j] > '9' { digits = false } } testing.expectf(t, !digits, "AGL name %q is shaped like a glyph index", e.name) } } for e, i in AGL_TABLE { testing.expectf(t, len(e.name) > 0, "empty AGL name at %d", i) testing.expectf(t, glyph_name_to_rune(e.name) == e.cp, "AGL %q -> U+%04X, want U+%04X", e.name, glyph_name_to_rune(e.name), e.cp) } } // A width table that is missing a glyph the encoding can name is the same bug as // a wrong width: the caller falls back to /MissingWidth, which a standard-14 // font dictionary usually does not carry, and the line collapses. @(test) test_every_encoded_glyph_has_a_width :: proc(t: ^testing.T) { // Seven glyphs are named by Mac OS Roman but are genuinely not in Adobe's // base 14 -- Helvetica has no infinity and no apple. A viewer draws .notdef // for them, so no width is the honest answer; they are listed rather than // skipped quietly so the rest of the sweep stays strict, and each one is // checked below to still be absent. absent := [?]string{"Omega", "apple", "approxequal", "infinity", "integral", "pi", "product"} is_absent :: proc(list: []string, name: string) -> bool { for x in list { if x == name { return true } } return false } for enc in ([?]^[256]string{encoding_standard(), encoding_winansi(), encoding_macroman()}) { for name, code in enc { if name == "" || is_absent(absent[:], name) { continue } for f in 0 ..< STD_SYMBOL { _, ok := std_font_width(f, name) testing.expectf(t, ok, "%s has no width for %q (code 0x%02X)", std_font_name(f), name, code) } testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) } } for name in absent { _, ok := std_font_width(STD_HELVETICA, name) testing.expectf(t, !ok, "%q is in Helvetica after all; drop it from the list", name) _, ok = std_font_width(STD_TIMES_ROMAN, name) testing.expectf(t, !ok, "%q is in Times-Roman after all; drop it from the list", name) testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q must still extract as text even with no width", name) } // The eight that MacRoman names and the fonts do carry must not be in that // list by accident. for name in ([?]string{"notequal", "lessequal", "greaterequal", "partialdiff", "summation", "radical", "Delta", "lozenge"}) { _, ok := std_font_width(STD_HELVETICA, name) testing.expectf(t, ok, "Helvetica does have %q", name) } for name in encoding_symbol() { if name == "" { continue } _, ok := std_font_width(STD_SYMBOL, name) testing.expectf(t, ok, "Symbol has no width for %q", name) testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) } for name in encoding_zapf() { if name == "" { continue } _, ok := std_font_width(STD_ZAPF_DINGBATS, name) testing.expectf(t, ok, "ZapfDingbats has no width for %q", name) testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) } // Every glyph the fonts do have must extract as text, or a page set in one // of them cannot be searched. for name in STD_TEXT_GLYPHS { testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) } for name in STD_SYMBOL_GLYPHS { testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) } for name in STD_ZAPF_GLYPHS { testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) } } // The helpers underneath the public four, checked directly: they are where a // name that came out of a file is taken apart, so they carry the bounds. @(test) test_std_name_helpers :: proc(t: ^testing.T) { testing.expect_value(t, std_strip_subset("ABCDEF+Helvetica"), "Helvetica") testing.expect_value(t, std_strip_subset("abcdef+Helvetica"), "Helvetica") testing.expect_value(t, std_strip_subset("AB+Helvetica"), "Helvetica") testing.expect_value(t, std_strip_subset("Helvetica"), "Helvetica") testing.expect_value(t, std_strip_subset(""), "") testing.expect_value(t, std_strip_subset("+Helvetica"), "+Helvetica") testing.expect_value(t, std_strip_subset("ABCDEF+"), "") // The plus must be within the tag, and only letters may precede it. testing.expect_value(t, std_strip_subset("Helvetica+Bold"), "Helvetica+Bold") testing.expect_value(t, std_strip_subset("AB1DEF+Helvetica"), "AB1DEF+Helvetica") buf: [8]u8 testing.expect_value(t, std_normalize("Arial-BoldMT", buf[:]), "arialbol") testing.expect_value(t, std_normalize("", buf[:]), "") big: [64]u8 testing.expect_value(t, std_normalize("Times New Roman,Bold", big[:]), "timesnewromanbold") testing.expect_value(t, std_normalize("\xff\x00-_.", big[:]), "") testing.expect(t, std_contains("helvetica", "vet"), "") testing.expect(t, std_contains("helvetica", "helvetica"), "") testing.expect(t, std_contains("helvetica", ""), "the empty needle is everywhere") testing.expect(t, !std_contains("helvetica", "helveticax"), "") testing.expect(t, !std_contains("", "a"), "") testing.expect(t, std_contains("aaab", "ab"), "") }