Files
pdf-odin-llm/font_standard_test.odin
T

846 lines
42 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
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"), "")
}