diff --git a/CHANGELOG.md b/CHANGELOG.md
index a15f365b..4220adce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -93,6 +93,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
the form's own `/Matrix` into the graphics state's CTM (§8.10.1 b) before interpreting its
content, so a caller reading the CTM from inside the form's own content sees the composed value,
not the invoker's own CTM with the form's matrix left for it to apply separately. (#98)
+- **Simple-font decoding for text extraction.** Type1, MMType1 and TrueType fonts map character
+ codes to glyph names through StandardEncoding, WinAnsiEncoding, MacRomanEncoding, the built-in
+ Symbol and ZapfDingbats encodings and `/Differences`, then to Unicode through the Adobe Glyph
+ List (ISO 32000-2 §9.6.5, §9.10.2, Annex D), with `/Widths` and the standard 14 metrics for
+ widths. Five diagnostic codes 400 to 404 report unreadable fonts, malformed encodings or
+ widths, fonts with no route to Unicode, and unmapped glyphs. Text extraction itself lands in a
+ later change. (#98)
### Changed
diff --git a/NOTICE b/NOTICE
index 6307cc30..13712375 100644
--- a/NOTICE
+++ b/NOTICE
@@ -31,9 +31,12 @@ Third-party data bundled in this product
Adobe Glyph List
Location : src/VellumPdf.Conformance/Resources/AdobeGlyphList.txt
+ src/VellumPdf.Reader/Resources/AdobeGlyphList.txt
+ (the Reader copy is byte-identical to the Conformance one)
Source : https://github.com/adobe-type-tools/agl-aglfn (glyphlist.txt)
Bundled copy extracted from veraPDF 1.30.2 (font/AdobeGlyphList.txt)
Use : Embedded resource for §7.21.6-2 glyph-name→Unicode compliance check
+ (Conformance) and the glyph-name Unicode route of §9.10.2 (Reader)
License : BSD 3-Clause
Copyright 2002-2019 Adobe (http://www.adobe.com/).
@@ -64,6 +67,70 @@ Adobe Glyph List
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+ZapfDingbats glyph list
+ Location : src/VellumPdf.Reader/Resources/ZapfDingbatsGlyphList.txt
+ Source : https://github.com/adobe-type-tools/agl-aglfn (zapfdingbats.txt)
+ commit 4036a9ca80a62f64f9de4f7321a9a045ad0ecfd6
+ SHA-256 : f6394e3cb8a447e84a1dad75d4baaf2aa7f45dc104faf369f4720e1a774ef2dc
+ (of the committed file, normalised to LF line endings)
+ Use : Unicode mapping for a ZapfDingbats-flagged simple font's glyph names
+ License : BSD 3-Clause, same copyright and terms as the Adobe Glyph List above.
+
+Adobe Core 14 AFM font metrics
+ Location : src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs
+ (a derived table, generated by eng/generate-symbol-font-metrics.py;
+ the AFM files themselves are not committed to this repository)
+ Source : Adobe Core 14 AFM files (MustRead.html, Adobe Systems, 1997)
+ Use : Built-in encodings and advance widths for the Symbol and
+ ZapfDingbats fonts (ISO 32000-2 Annex D.5, D.6) and advance widths
+ for the twelve text fonts of the standard 14 when a font
+ dictionary omits /Widths (§9.6.2.2)
+
+ Symbol.afm:
+ Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.
+ All rights reserved.
+
+ Helvetica.afm, Helvetica-Bold.afm, Helvetica-Oblique.afm,
+ Helvetica-BoldOblique.afm:
+ Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems
+ Incorporated. All Rights Reserved.
+
+ ZapfDingbats.afm:
+ Comment Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated.
+ All Rights Reserved.
+
+ Times-Roman.afm, Times-Bold.afm, Times-Italic.afm, Times-BoldItalic.afm:
+ Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems
+ Incorporated. All Rights Reserved.
+
+ Courier.afm, Courier-Oblique.afm:
+ Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems
+ Incorporated. All Rights Reserved.
+
+ Courier-Bold.afm, Courier-BoldOblique.afm:
+ Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems
+ Incorporated. All Rights Reserved.
+
+ Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.
+ Times is a trademark of Linotype-Hell AG and/or its subsidiaries.
+ ITC Zapf Dingbats is a registered trademark of International Typeface
+ Corporation.
+
+ This file and the 14 PostScript(R) AFM files it accompanies may be used,
+ copied, and distributed for any purpose and without charge, with or without
+ modification, provided that all copyright notices are retained; that the AFM
+ files are not distributed without this file; that all modifications to this
+ file or any of the AFM files are prominently noted in the modified file(s);
+ and that this paragraph is not modified. Adobe Systems has no responsibility
+ or obligation to support the use of the AFM files.
+
+ That licence conditions redistribution of the AFM files themselves on all
+ copyright notices being retained, and SymbolFontMetrics.cs is a derived
+ table of glyph names, codes and advance widths rather than a copy of those
+ files, so the condition does not reach it. This entry reproduces the
+ notices anyway, because a reader of the generated table has no other way to
+ learn where its numbers came from.
+
────────────────────────────────────────────────────────────────────────────────
Third-party data used to build documentation (not bundled)
────────────────────────────────────────────────────────────────────────────────
diff --git a/eng/generate-symbol-font-metrics.py b/eng/generate-symbol-font-metrics.py
new file mode 100644
index 00000000..ce1f3b00
--- /dev/null
+++ b/eng/generate-symbol-font-metrics.py
@@ -0,0 +1,426 @@
+# Copyright © Timothy van der Ham (@Tim81)
+# SPDX-License-Identifier: Apache-2.0
+#
+# Generates src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs from all fourteen Adobe Core-14 AFM
+# files (MustRead.html, Adobe Systems, 1997).
+#
+# Symbol.afm and ZapfDingbats.afm are the two symbolic standard 14 fonts (ISO 32000-2 Table 121
+# bit 3): their built-in encodings are Annex D.5 and D.6, and the AFM's own C records are this
+# reader's delivery vehicle for the same glyph-name/code/width data, not a separate transcription
+# of the Annex D tables. This script also emits a glyph-name-keyed width table for each of the
+# twelve nonsymbolic text fonts, keyed by name rather than by Unicode code point, so a name the
+# text font's own encoding assigns (by /Differences or otherwise) measures at its own AFM width
+# regardless of whether that name's Unicode value happens to fall inside WinAnsiEncoding.
+#
+# The AFM files themselves are NOT committed to this repository (their own licence permits
+# copying and redistribution provided the copyright notices are retained and this file's own
+# paragraph travels with them, but this project ships only the derived table this script
+# produces, not the AFM files verbatim). This script re-derives that table from a local copy
+# supplied at generation time and pins every source file with a normalised SHA-256 manifest, so a
+# substituted or edited AFM file fails loudly instead of silently changing the emitted table.
+#
+# Usage:
+# python eng/generate-symbol-font-metrics.py --afm-dir
--out # regenerate
+# python eng/generate-symbol-font-metrics.py --afm-dir --check # verify up to date
+#
+# holds all fourteen Adobe Core-14 AFM files (MustRead.html, Adobe Systems, 1997).
+# defaults to src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs.
+
+import hashlib
+import os
+import re
+import sys
+
+DEFAULT_OUTPUT = "src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs"
+
+# (AFM filename, the name this reader resolves it to (Standard14Names' own strings for the
+# twelve text fonts), the generated field-name fragment, whether it carries a built-in encoding).
+FONT_TABLE = [
+ ("Symbol.afm", "Symbol", "symbol", True),
+ ("ZapfDingbats.afm", "ZapfDingbats", "zapfDingbats", True),
+ ("Helvetica.afm", "Helvetica", "helvetica", False),
+ ("Helvetica-Bold.afm", "Helvetica-Bold", "helveticaBold", False),
+ ("Helvetica-Oblique.afm", "Helvetica-Oblique", "helveticaOblique", False),
+ ("Helvetica-BoldOblique.afm", "Helvetica-BoldOblique", "helveticaBoldOblique", False),
+ ("Times-Roman.afm", "Times-Roman", "timesRoman", False),
+ ("Times-Bold.afm", "Times-Bold", "timesBold", False),
+ ("Times-Italic.afm", "Times-Italic", "timesItalic", False),
+ ("Times-BoldItalic.afm", "Times-BoldItalic", "timesBoldItalic", False),
+ ("Courier.afm", "Courier", "courier", False),
+ ("Courier-Bold.afm", "Courier-Bold", "courierBold", False),
+ ("Courier-Oblique.afm", "Courier-Oblique", "courierOblique", False),
+ ("Courier-BoldOblique.afm", "Courier-BoldOblique", "courierBoldOblique", False),
+]
+
+# Normalised SHA-256 of each AFM file: split on any of CR LF, LF, CR; strip trailing whitespace
+# per line; drop empty lines; join with a single LF; append one trailing LF. Guards against a
+# substituted or hand-edited input file changing the emitted table without being noticed.
+MANIFEST = {
+ "Symbol.afm": "a336805b37aa468ba403bcae995652e9b335994462ab3703a572ed5bb87363d7",
+ "ZapfDingbats.afm": "b56fbcaebd71b210ba4cfac4bb669764c4f1bd7ab523f37f01b2f04f079bf699",
+ "Helvetica.afm": "79e23df3e75df921fb8666fceb35b7ca363737dc9b5519cd4c1d9d1d2c23599c",
+ "Helvetica-Bold.afm": "5f455fcdbf5583c3b9bc5d884d352eef3aed330396f0467328fac89eb6f4c740",
+ "Helvetica-Oblique.afm": "91ed92f7b46b73fe062ad22667c2c462eafc0197288d9d70b989209b83e767f4",
+ "Helvetica-BoldOblique.afm": "910c39255d66d14b08e64ced7ecca0511f974940ca5da93c82bb41e61519d9f2",
+ "Times-Roman.afm": "41928f144173929c15f8cd4cc19aafeead3bd82b22141e55c538e94af7449ecf",
+ "Times-Bold.afm": "c29b638a607a3347fa42a57adbfe98779681fa99b8b7c0ef903ff6ec6075d01c",
+ "Times-Italic.afm": "74d5c65a553790bf3844cc515b591fcf9b2cc7b9b8472e114bdc231d5bb551af",
+ "Times-BoldItalic.afm": "ea8b71d0a1f8cdb6835bec1f37dcef6ab1eeee1bca3c6f88b07a85eef4d1455e",
+ "Courier.afm": "bdeeadc7738eccd3deb220e26d65152e6cbafaffc70d9f6c73a5461a893f16eb",
+ "Courier-Bold.afm": "132f204bdeb88061b6180d7e8a9dd1a6fd313c5e8da5a64af0c0464b8155d879",
+ "Courier-Oblique.afm": "728a2c018b50f40368dbdaa393bb97937e713c2e0f4841e40e4abafe02fdb09b",
+ "Courier-BoldOblique.afm": "780a896a2a331066e8ed39024833ba6864fb4286f775d285186fae5aa348c436",
+}
+
+EXPECTED_RECORD_COUNT = {
+ "Symbol.afm": 190,
+ "ZapfDingbats.afm": 202,
+ "Helvetica.afm": 315,
+ "Helvetica-Bold.afm": 315,
+ "Helvetica-Oblique.afm": 315,
+ "Helvetica-BoldOblique.afm": 315,
+ "Times-Roman.afm": 315,
+ "Times-Bold.afm": 315,
+ "Times-Italic.afm": 315,
+ "Times-BoldItalic.afm": 315,
+ "Courier.afm": 315,
+ "Courier-Bold.afm": 315,
+ "Courier-Oblique.afm": 315,
+ "Courier-BoldOblique.afm": 315,
+}
+
+# The MustRead.html paragraph, verbatim (the licence text governing use of the AFM files this
+# script reads; it requires copyright notices to be retained and this paragraph itself to travel
+# unmodified alongside them, which is why it is reproduced in full in the generated file).
+MUSTREAD_PARAGRAPH = (
+ "This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and "
+ "distributed for any purpose and without charge, with or without modification, provided "
+ "that all copyright notices are retained; that the AFM files are not distributed without "
+ "this file; that all modifications to this file or any of the AFM files are prominently "
+ "noted in the modified file(s); and that this paragraph is not modified. Adobe Systems has "
+ "no responsibility or obligation to support the use of the AFM files."
+)
+
+C_RECORD = re.compile(r"^C (-?\d+) ; WX (-?\d+) ; N (\S+) ;")
+VERSION_LINE = re.compile(r"^Version (\S+)$")
+
+# The AFM's own Notice line carries the copyright sentence (also duplicated onto its own Comment
+# Copyright line, read separately below) immediately followed, with no separating space, by a
+# trademark sentence where the font has one: Helvetica's four files name Linotype-Hell AG,
+# Times' four name it too, and ZapfDingbats names International Typeface Corporation. Symbol and
+# the four Courier files carry no trademark sentence at all. This captures whatever follows
+# "Reserved." on that line, case-insensitive in "Rights"/"rights" and "Reserved"/"reserved" only
+# ("All" itself is capitalised the same way in every one of the fourteen files).
+TRADEMARK_AFTER_RESERVED = re.compile(r"All [Rr]ights [Rr]eserved\.(.*)$")
+
+
+def normalize(raw_bytes):
+ text = raw_bytes.decode("latin-1")
+ lines = re.split(r"\r\n|\r|\n", text)
+ lines = [line.rstrip() for line in lines]
+ lines = [line for line in lines if line != ""]
+ return "\n".join(lines) + "\n"
+
+
+def load_afm(afm_dir, filename):
+ path = os.path.join(afm_dir, filename)
+ with open(path, "rb") as f:
+ raw = f.read()
+ normalized = normalize(raw)
+ actual_hash = hashlib.sha256(normalized.encode("latin-1")).hexdigest()
+ expected_hash = MANIFEST[filename]
+ if actual_hash != expected_hash:
+ print(
+ f"{filename}: normalised SHA-256 is {actual_hash}, expected {expected_hash}. "
+ "Refusing to generate from an AFM file that does not match the pinned manifest.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ return normalized
+
+
+def parse_afm(filename, normalized):
+ copyright_line = None
+ notice_line = None
+ version = None
+ records = []
+ seen_names = set()
+ for line in normalized.split("\n"):
+ if line.startswith("Comment Copyright") and copyright_line is None:
+ copyright_line = line
+ if line.startswith("Notice") and notice_line is None:
+ notice_line = line
+ if version is None:
+ m = VERSION_LINE.match(line)
+ if m:
+ version = m.group(1)
+ if not line.startswith("C "):
+ continue
+ m = C_RECORD.match(line)
+ if not m:
+ print(f"{filename}: unparsable C record: {line!r}", file=sys.stderr)
+ sys.exit(1)
+ code, width, name = int(m.group(1)), int(m.group(2)), m.group(3)
+ if not -1 <= code <= 255:
+ print(f"{filename}: C record code {code} outside -1..255: {line!r}", file=sys.stderr)
+ sys.exit(1)
+ if name in seen_names:
+ print(f"{filename}: duplicate glyph name {name!r}", file=sys.stderr)
+ sys.exit(1)
+ seen_names.add(name)
+ records.append((code, width, name))
+
+ if copyright_line is None:
+ print(f"{filename}: no Comment Copyright line found", file=sys.stderr)
+ sys.exit(1)
+ if notice_line is None:
+ print(f"{filename}: no Notice line found", file=sys.stderr)
+ sys.exit(1)
+ if version is None:
+ print(f"{filename}: no Version line found", file=sys.stderr)
+ sys.exit(1)
+
+ expected = EXPECTED_RECORD_COUNT[filename]
+ if len(records) != expected:
+ print(
+ f"{filename}: {len(records)} C records, expected exactly {expected}", file=sys.stderr
+ )
+ sys.exit(1)
+
+ m = TRADEMARK_AFTER_RESERVED.search(notice_line)
+ trademark = m.group(1).strip() if m else ""
+ trademark = trademark if trademark else None
+
+ return records, copyright_line, version, trademark
+
+
+def format_encoding(field_name, records):
+ lines = [f" private static readonly string?[] _{field_name} = BuildEncoding_{field_name}();", ""]
+ coded = sorted((code, name) for code, _, name in records if code != -1)
+ body = [f" private static string?[] BuildEncoding_{field_name}()", " {", " var t = new string?[256];"]
+ for code, name in coded:
+ body.append(f' t[0x{code:02X}] = "{name}";')
+ body.append(" return t;")
+ body.append(" }")
+ return lines, body
+
+
+def format_widths(field_name, records):
+ body = [
+ f" private static readonly FrozenDictionary _{field_name}Widths = "
+ "new Dictionary",
+ " {",
+ ]
+ for _, width, name in records:
+ body.append(f' ["{name}"] = {width},')
+ body.append(" }.ToFrozenDictionary();")
+ return body
+
+
+def wrap_comment(text, width=96):
+ words = text.split(" ")
+ lines = []
+ current = "// "
+ for word in words:
+ candidate = f"{current}{word} " if current != "// " else f"{current}{word} "
+ if len(candidate.rstrip()) > width and current != "// ":
+ lines.append(current.rstrip())
+ current = f"// {word} "
+ else:
+ current = candidate
+ if current.strip() != "//":
+ lines.append(current.rstrip())
+ return lines
+
+
+def generate_source(parsed):
+ # parsed: filename -> (records, copyright_line, version, trademark), in FONT_TABLE order.
+ o = []
+ w = o.append
+
+ w("// Copyright © Timothy van der Ham (@Tim81)")
+ w("// SPDX-License-Identifier: Apache-2.0")
+ w("")
+ w("using System.Collections.Frozen;")
+ w("")
+ w("// Generated by eng/generate-symbol-font-metrics.py; do not edit by hand.")
+ w("//")
+ w("// Inputs (Adobe Core-14 AFM files, MustRead.html, Adobe Systems, 1997), each one's own")
+ w("// Version line, and the normalised SHA-256 this generator pinned it against:")
+ for filename, _, _, _ in FONT_TABLE:
+ _, _, version, _ = parsed[filename]
+ w(f"// {filename} (Version {version})")
+ w(f"// {MANIFEST[filename]}")
+ w("//")
+ for filename, _, _, _ in FONT_TABLE:
+ _, copyright_line, _, trademark = parsed[filename]
+ for line in wrap_comment(f"{filename}: {copyright_line}"):
+ w(line)
+ if trademark:
+ for line in wrap_comment(f"{filename}: {trademark}"):
+ w(line)
+ w("//")
+ for line in wrap_comment(MUSTREAD_PARAGRAPH):
+ w(line)
+ w("//")
+ w("// This file is a derived table of glyph names, codes and advance widths, not a copy of")
+ w("// the AFM files.")
+ w("")
+ w("namespace VellumPdf.Reader.Fonts;")
+ w("")
+ w("/// ")
+ w("/// The built-in encodings of the two symbolic standard 14 fonts (Symbol and ZapfDingbats)")
+ w("/// and the glyph-name-keyed advance widths of all fourteen. ISO 32000-2 Annex D.1 names")
+ w("/// Annex D.5 and D.6 as the symbolic pair's built-in encodings; the Adobe Core-14 AFM")
+ w("/// files are this reader's delivery vehicle for that same data, not a separate")
+ w("/// transcription of the Annex D tables. The Symbol coding here agrees with Annex D.5 at")
+ w("/// all 189 coded glyphs. The ZapfDingbats coding carries 14 codes (0x80 to 0x8D) that")
+ w("/// Annex D.6 does not document at all; this reader keeps them, on the view that a font")
+ w("/// program carrying those codes draws them regardless of whether the standard's own")
+ w("/// table lists them. The twelve nonsymbolic text fonts carry no built-in encoding here")
+ w("/// (their glyph names come from and /Differences")
+ w("/// instead); only their AFM widths are kept, keyed by glyph name so a name outside")
+ w("/// WinAnsiEncoding still measures at its own width rather than a substitute glyph's.")
+ w("/// ")
+ w("internal static class SymbolFontMetrics")
+ w("{")
+
+ symbol_records = parsed["Symbol.afm"][0]
+ zapf_records = parsed["ZapfDingbats.afm"][0]
+ symbol_enc_decl, symbol_enc_body = format_encoding("symbol", symbol_records)
+ zapf_enc_decl, zapf_enc_body = format_encoding("zapfDingbats", zapf_records)
+
+ w(" /// Symbol's built-in encoding (ISO 32000-2 Annex D.5): char code to glyph")
+ w(" /// name, null where the AFM assigns the code no glyph.")
+ w(" public static ReadOnlySpan SymbolEncoding => _symbol;")
+ w("")
+ w(" /// ZapfDingbats' built-in encoding (ISO 32000-2 Annex D.6, plus the 14 codes")
+ w(" /// the class doc above names): char code to glyph name.")
+ w(" public static ReadOnlySpan ZapfDingbatsEncoding => _zapfDingbats;")
+ w("")
+ w(" /// Symbol's AFM advance widths, name-keyed (includes \"apple\", which the")
+ w(" /// AFM assigns no code (C -1), so it is absent from")
+ w(" /// ).")
+ w(" public static IReadOnlyDictionary SymbolWidths => _symbolWidths;")
+ w("")
+ w(" /// ZapfDingbats' AFM advance widths, name-keyed.")
+ w(" public static IReadOnlyDictionary ZapfDingbatsWidths => _zapfDingbatsWidths;")
+ w("")
+ w(" /// ")
+ w(" /// The named text font's AFM advance widths, keyed by glyph name (the AFM's own N")
+ w(" /// records, including names the font's own StandardEncoding table has no code for).")
+ w(" /// is one of the twelve exact names")
+ w(" /// Standard14Names.TryResolve can produce (e.g. \"Helvetica-Bold\").")
+ w(" /// Returns for \"Symbol\", \"ZapfDingbats\", or")
+ w(" /// any other name.")
+ w(" /// ")
+ w(" public static bool TryGetTextFontWidths(string afmName, out IReadOnlyDictionary widths)")
+ w(" {")
+ w(" if (_textFontWidths.TryGetValue(afmName, out var found))")
+ w(" {")
+ w(" widths = found;")
+ w(" return true;")
+ w(" }")
+ w(" widths = FrozenDictionary.Empty;")
+ w(" return false;")
+ w(" }")
+ w("")
+ for line in symbol_enc_decl:
+ w(line)
+ for line in zapf_enc_decl:
+ w(line)
+
+ for filename, _, field_name, has_encoding in FONT_TABLE:
+ if has_encoding:
+ continue
+ records = parsed[filename][0]
+ for line in format_widths(field_name, records):
+ w(line)
+ w("")
+
+ for line in format_widths("symbol", symbol_records):
+ w(line)
+ w("")
+ for line in format_widths("zapfDingbats", zapf_records):
+ w(line)
+ w("")
+
+ w(" private static readonly FrozenDictionary> _textFontWidths =")
+ w(" new Dictionary>")
+ w(" {")
+ for filename, _, field_name, has_encoding in FONT_TABLE:
+ if has_encoding:
+ continue
+ afm_name = next(n for f, n, fld, _ in FONT_TABLE if fld == field_name)
+ w(f' ["{afm_name}"] = _{field_name}Widths,')
+ w(" }.ToFrozenDictionary();")
+ w("")
+
+ for line in symbol_enc_body:
+ w(line)
+ w("")
+ for line in zapf_enc_body:
+ w(line)
+ w("}")
+ return "\n".join(o) + "\n"
+
+
+def main():
+ args = sys.argv[1:]
+ afm_dir = None
+ output = DEFAULT_OUTPUT
+ check = False
+ i = 0
+ while i < len(args):
+ if args[i] == "--afm-dir" and i + 1 < len(args):
+ afm_dir = args[i + 1]
+ i += 2
+ elif args[i] == "--out" and i + 1 < len(args):
+ output = args[i + 1]
+ i += 2
+ elif args[i] == "--check":
+ check = True
+ i += 1
+ else:
+ print(f"unrecognised argument: {args[i]}", file=sys.stderr)
+ return 1
+
+ if afm_dir is None:
+ print("usage: generate-symbol-font-metrics.py --afm-dir [--out | --check]", file=sys.stderr)
+ return 1
+
+ parsed = {}
+ for filename, _, _, _ in FONT_TABLE:
+ normalized = load_afm(afm_dir, filename)
+ parsed[filename] = parse_afm(filename, normalized)
+
+ text = generate_source(parsed)
+
+ if check:
+ if not os.path.exists(output):
+ print(f"{output} does not exist; run without --check to generate it.", file=sys.stderr)
+ return 1
+ with open(output, encoding="utf-8") as f:
+ existing = f.read()
+ if existing == text:
+ print(f"{output} is up to date.")
+ return 0
+ import difflib
+
+ diff = difflib.unified_diff(
+ existing.splitlines(keepends=True), text.splitlines(keepends=True),
+ fromfile=output, tofile=f"{output} (generated)",
+ )
+ sys.stdout.writelines(diff)
+ print(f"{output} is out of date; run without --check to regenerate.", file=sys.stderr)
+ return 1
+
+ os.makedirs(os.path.dirname(output), exist_ok=True) if os.path.dirname(output) else None
+ with open(output, "w", encoding="utf-8", newline="\n") as f:
+ f.write(text)
+ print(f"Wrote {output}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs
new file mode 100644
index 00000000..8ea8c15a
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs
@@ -0,0 +1,246 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Reflection;
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// Maps a glyph name to Unicode per the Adobe Glyph List (AGL) Specification, for the glyph-name
+/// route of ISO 32000-2 §9.10.2. Backed by the embedded AdobeGlyphList.txt resource, the
+/// same file src/VellumPdf.Conformance/Resources/AdobeGlyphList.txt ships (copied
+/// byte-for-byte; see NOTICE), parsed once per process into a name-to-Unicode-string dictionary of
+/// 4282 entries. 81 of those entries carry more than one code point (mostly Hebrew
+/// letter-plus-point combinations, a base letter in the U+05xx block followed by its own
+/// combining point), so the map's value is a string, not a single .
+///
+///
+/// This reader's own departs from the AGL Specification's algorithm
+/// in two ways, each because the two ends of the departure are indistinguishable to a caller that
+/// only gets a Unicode string back:
+///
+/// - A component with no mapping fails the whole name. The AGL Specification
+/// maps such a component to the empty string and continues, but an empty string is
+/// indistinguishable from a mapped control character once concatenated into the result, so this
+/// reader treats it as no mapping instead.
+/// - A result of exactly U+0000 is also treated as no mapping. This covers the
+/// literal names uni0000 and u0000. .notdef never reaches this rule: its
+/// leading . is the first character, so the dot-suffix trim leaves an empty name, rejected
+/// by the empty-name check before any component is looked up at all.
+///
+/// Accepting only uppercase uni/u hex digits is not a third departure: the AGL
+/// Specification itself requires "a sequence of uppercase hexadecimal digits" for both synthetic
+/// forms. The Conformance package's own copy
+/// (src/VellumPdf.Conformance/Rules/Fonts/AdobeGlyphList.cs) additionally accepts
+/// lowercase; on hex-digit case, this reader is the stricter of the two, not the looser.
+///
+internal static class AdobeGlyphList
+{
+ ///
+ /// The longest glyph name this reader accepts. A uniXXXX name is 3 + 4k
+ /// characters for k hex groups, so 127 (31 groups) is the longest accepted and 131 the
+ /// shortest rejected; an underscore-joined chain of single-character components follows the
+ /// same bound per component count (64 components of one character each is 127 characters with
+ /// 63 joining underscores).
+ ///
+ public const int MaxGlyphNameLength = 128;
+
+ private static readonly Lazy> _map = new(Load, isThreadSafe: true);
+
+ /// Entry count of the loaded list: test-only visibility for pinning its size (4282)
+ /// and the count of multi-code-point entries (81) directly, rather than through
+ /// behaviour.
+ internal static int Count => _map.Value.Count;
+
+ ///
+ /// Maps to Unicode per the AGL Specification: the name is
+ /// truncated at the first . (a production tag, e.g. f.alt or uni0041.sc),
+ /// split on _, and each component is looked up in the list, then as uniXXXX (one
+ /// or more 4-hex-digit groups, uppercase, each in 0000..D7FF or E000..FFFF), then
+ /// as uXXXX through uXXXXXX (uppercase, 0000..10FFFF excluding
+ /// surrogates). Returns when the name is longer than
+ /// , any component has no mapping, any component is empty (a
+ /// leading, trailing, or doubled _), or the mapped result is exactly U+0000.
+ ///
+ public static bool TryMapToUnicode(string glyphName, out string unicode)
+ {
+ unicode = "";
+ if (glyphName.Length == 0 || glyphName.Length > MaxGlyphNameLength)
+ return false;
+
+ var dot = glyphName.IndexOf('.');
+ var trimmed = dot < 0 ? glyphName : glyphName[..dot];
+ if (trimmed.Length == 0)
+ return false;
+
+ var map = _map.Value;
+
+ // The common case is a single component (no '_'): return TryMapComponent's own string
+ // directly, most often the map's own value for the name, rather than copying it through a
+ // StringBuilder. Routing every single-component name through a StringBuilder instead is
+ // the single largest source of FontCache's per-font retained bytes (see
+ // FontCache.MaxCachedFonts).
+ if (trimmed.IndexOf('_') < 0)
+ {
+ if (!TryMapComponent(map, trimmed, out var single))
+ return false;
+ // uni0000 and u0000 resolve to U+0000 here and are treated as unmapped (the class
+ // remarks); .notdef never reaches this line at all, its trimmed name is already empty.
+ if (single.Length == 1 && single[0] == '\0')
+ return false;
+ unicode = single;
+ return true;
+ }
+
+ var result = new System.Text.StringBuilder();
+ var start = 0;
+ while (start <= trimmed.Length)
+ {
+ var underscore = trimmed.IndexOf('_', start);
+ var end = underscore < 0 ? trimmed.Length : underscore;
+ if (end == start)
+ return false; // empty component: leading, trailing, or doubled '_'
+
+ var component = trimmed[start..end];
+ if (!TryMapComponent(map, component, out var piece))
+ return false;
+ result.Append(piece);
+
+ if (underscore < 0)
+ break;
+ start = underscore + 1;
+ }
+
+ // Only a whole result of exactly one U+0000 character is rejected here; two components
+ // that each resolve to U+0000 (uni0000_uni0000) concatenate to a two-character result and
+ // are not caught by this check (AdobeGlyphListTests pins that shape directly).
+ if (result.Length == 1 && result[0] == '\0')
+ return false;
+
+ unicode = result.ToString();
+ return true;
+ }
+
+ private static bool TryMapComponent(Dictionary map, string component, out string piece)
+ {
+ if (map.TryGetValue(component, out var mapped))
+ {
+ piece = mapped;
+ return true;
+ }
+
+ if (TryUniName(component, out var uni))
+ {
+ piece = uni;
+ return true;
+ }
+
+ if (TryUName(component, out var cp))
+ {
+ piece = char.ConvertFromUtf32(cp);
+ return true;
+ }
+
+ piece = "";
+ return false;
+ }
+
+ private static bool TryUniName(string component, out string unicode)
+ {
+ unicode = "";
+ // "uni" + one or more 4-hex-digit groups, each mapped independently and concatenated:
+ // uni00660066 is "ff", the same result "f_f" would give through the AGL list itself.
+ if (component.Length < 7 || (component.Length - 3) % 4 != 0
+ || !component.StartsWith("uni", StringComparison.Ordinal))
+ return false;
+
+ var groups = (component.Length - 3) / 4;
+ var sb = new System.Text.StringBuilder(groups);
+ for (var g = 0; g < groups; g++)
+ {
+ if (!TryParseHex4(component, 3 + g * 4, out var cp))
+ return false;
+ if (cp is >= 0xD800 and <= 0xDFFF)
+ return false; // a surrogate group is not a valid BMP scalar on its own.
+ sb.Append((char)cp);
+ }
+
+ unicode = sb.ToString();
+ return true;
+ }
+
+ private static bool TryUName(string component, out int codePoint)
+ {
+ codePoint = 0;
+ if (component.Length < 5 || component.Length > 7
+ || !component.StartsWith("u", StringComparison.Ordinal)
+ || component.StartsWith("uni", StringComparison.Ordinal))
+ return false;
+
+ var hexLen = component.Length - 1;
+ if (!TryParseHex(component, 1, hexLen, out var cp))
+ return false;
+ if (cp > 0x10FFFF || (cp is >= 0xD800 and <= 0xDFFF))
+ return false;
+
+ codePoint = cp;
+ return true;
+ }
+
+ private static bool TryParseHex4(string s, int start, out int value) => TryParseHex(s, start, 4, out value);
+
+ private static bool TryParseHex(string s, int start, int length, out int value)
+ {
+ value = 0;
+ for (var i = start; i < start + length; i++)
+ {
+ var c = s[i];
+ int digit;
+ if (c is >= '0' and <= '9') digit = c - '0';
+ else if (c is >= 'A' and <= 'F') digit = c - 'A' + 10;
+ else return false; // lowercase a-f deliberately rejected; see this class's own remarks.
+ value = (value << 4) | digit;
+ }
+ return true;
+ }
+
+ private static Dictionary Load()
+ {
+ var map = new Dictionary(4300, StringComparer.Ordinal);
+ var asm = Assembly.GetExecutingAssembly();
+ using var stream = asm.GetManifestResourceStream("AdobeGlyphList.txt");
+ if (stream is null)
+ return map;
+
+ using var reader = new StreamReader(stream, System.Text.Encoding.ASCII, detectEncodingFromByteOrderMarks: false);
+ string? line;
+ while ((line = reader.ReadLine()) is not null)
+ {
+ if (line.Length == 0 || line[0] == '#')
+ continue;
+ var space = line.IndexOf(' ');
+ if (space <= 0 || space >= line.Length - 1)
+ continue;
+
+ var name = line[..space];
+ var codes = line[(space + 1)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ var sb = new System.Text.StringBuilder(codes.Length);
+ var ok = true;
+ foreach (var code in codes)
+ {
+ if (!int.TryParse(code, System.Globalization.NumberStyles.HexNumber, null, out var cp))
+ {
+ ok = false;
+ break;
+ }
+ // Unguarded: pinned by byte-identity with the Conformance copy
+ // (ReaderEncodingParityTests) and by ListSize_is4282, which fails if a bad value
+ // throws out of Load; never a surrogate half or a value past 0x10FFFF.
+ sb.Append(char.ConvertFromUtf32(cp));
+ }
+ if (ok)
+ map[name] = sb.ToString();
+ }
+ return map;
+ }
+}
diff --git a/src/VellumPdf.Reader/Fonts/FontCache.cs b/src/VellumPdf.Reader/Fonts/FontCache.cs
new file mode 100644
index 00000000..a62ddb5e
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/FontCache.cs
@@ -0,0 +1,62 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// Caches a per indirect font object, so a page that shows text with
+/// the same /Font resource repeatedly (the common case) builds it once. Keyed on
+/// (objectNumber, generation); a direct font dictionary (no object number, legal per ISO
+/// 32000-2 §7.8.3 though unusual) is never cached, since there is no stable key to cache it
+/// under, and is rebuilt on every lookup.
+///
+///
+/// Insert-only: past entries, a lookup still builds and returns a
+/// reader, just without adding it to the cache. This is a deliberate departure from evicting the
+/// least-recently-used entry: an LRU cache is more machinery than a document with more than
+/// 10,000 distinct font objects (far past what any PDF in practice carries) is worth building for,
+/// and the fallback costs only a rebuilt reader, not a wrong one.
+///
+/// Retained size at the cap, measured with GC.GetTotalMemory(true) before and after
+/// building 10,000 fonts and keeping every one reachable: a bare SimpleFontReader retains
+/// about 6,464 B each (about 62 MiB total); a full 10,000-entry cache built through
+/// PdfDocumentReader.GetFontReader, which also grows the reader's own resolved-object
+/// cache alongside it, retains about 7,639 B per font (about 73 MiB total).
+/// AdobeGlyphList.TryMapToUnicode returns the mapped string for a single-component glyph
+/// name directly rather than routing it through a StringBuilder, which accounts for most
+/// of that per-font retained cost.
+///
+///
+/// A font PdfDocumentReader.GetFontReader itself rejects (an unreadable font resource, or
+/// a /Subtype naming a type this reader does not know) never reaches
+/// at all, so it is not cached here: that rejection's diagnostic is
+/// reported again on every page that names the same font object, unlike a font whose own
+/// SimpleFontReader.Create reported a diagnostic, which is cached like any other. Not
+/// thread-safe, like every other cache the reader keeps.
+///
+///
+internal sealed class FontCache
+{
+ internal const int MaxCachedFonts = 10_000;
+
+ private readonly Dictionary<(int ObjectNumber, int Generation), PdfFontReader> _cache = [];
+
+ /// Returns the cached reader for this object number and generation when one exists;
+ /// otherwise builds one with , caching it unless the font dictionary
+ /// was direct ( is ) or the cache is
+ /// already at .
+ internal PdfFontReader GetOrCreate(int? objectNumber, int? generation, Func build)
+ {
+ if (objectNumber is null)
+ return build();
+
+ var key = (objectNumber.Value, generation ?? 0);
+ if (_cache.TryGetValue(key, out var cached))
+ return cached;
+
+ var built = build();
+ if (_cache.Count < MaxCachedFonts)
+ _cache[key] = built;
+ return built;
+ }
+}
diff --git a/src/VellumPdf.Reader/Fonts/PdfFontReader.cs b/src/VellumPdf.Reader/Fonts/PdfFontReader.cs
new file mode 100644
index 00000000..4d84dfe5
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/PdfFontReader.cs
@@ -0,0 +1,48 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// One decoded glyph from a content-stream string operand (ISO 32000-2 §9.4.3).
+///
+/// The character code as read from the string.
+/// Bytes consumed from the string for this code; always 1 for a simple
+/// font (§9.6.5), since every code in a simple font is a single byte.
+/// The glyph's advance width, in the thousandths-of-text-space unit
+/// /Widths itself uses (§9.6.2.1 Table 109). Always populated: MissingWidth (0
+/// unless the font descriptor overrides it) when the font gives this code no width of its own,
+/// never .
+/// The code's Unicode mapping, or when no route maps
+/// it (§9.10.2). populates this from the glyph-name route only
+/// (the AGL, or the ZapfDingbats list); the higher-priority /ToUnicode route is not parsed
+/// yet (#98), and records whether the font names
+/// one.
+/// Whether this is the single-byte code 32, the word-spacing code
+/// Tw applies to (§9.3.3) for a simple font.
+internal readonly record struct DecodedGlyph(
+ int Code, int CodeLength, double Width, string? Unicode, bool IsSpaceCode);
+
+///
+/// Decodes glyphs from a font's string operands. One instance is built per distinct font resource
+/// (see ) and reused across every string shown with it.
+///
+internal abstract class PdfFontReader
+{
+ ///
+ /// Decodes the next glyph starting at into ,
+ /// advancing past the bytes consumed. Returns
+ /// without advancing when it is already at the end of
+ /// .
+ ///
+ public abstract bool TryDecodeNext(ReadOnlySpan bytes, ref int offset, out DecodedGlyph glyph);
+
+ ///
+ /// Whether this font's dictionary names a /ToUnicode stream (§9.10.3). Recorded, not
+ /// parsed yet (#98): once parsed, that stream takes priority over the glyph-name route in
+ /// , per §9.10.2's own ordering. Until then a
+ /// here suppresses ,
+ /// since the unparsed stream may map the code.
+ ///
+ public abstract bool HasToUnicode { get; }
+}
diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs b/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs
new file mode 100644
index 00000000..f1a88729
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs
@@ -0,0 +1,287 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// The predefined simple-font encodings of ISO 32000-2:2020 Annex D.2 (Latin character set and
+/// encodings): StandardEncoding, WinAnsiEncoding and MacRomanEncoding, each a 256-entry char-code
+/// to glyph-name table, plus MacExpertEncoding, which this reader recognises by name only (see
+/// ). Symbol and ZapfDingbats are not named encodings a font's own
+/// /Encoding entry can select; their built-in encodings live in
+/// instead. §9.6.5 lists three predefined encoding names
+/// (MacRomanEncoding, MacExpertEncoding, WinAnsiEncoding), and Annex D.1 says "PDF processors
+/// shall not have a predefined encoding named StandardEncoding";
+/// accepts StandardEncoding by name anyway, silently, as a leniency toward producers that
+/// write it. The table itself is needed regardless, as the default base encoding of Table 112.
+///
+///
+/// Transcribed from the Annex D.2 table (rendered page images, not the Conformance package's copy)
+/// with three footnotes applied to WinAnsiEncoding. Footnote 3, verbatim: "In WinAnsiEncoding, all
+/// unused codes greater than 40 map to the bullet character. However, only code 225 is specifically
+/// assigned to the bullet character; other codes are subject to future reassignment." (40 and 225
+/// are octal: 0x20 and 0x95.) Codes 0x7F, 0x81, 0x8D, 0x8F, 0x90 and 0x9D are the codes that
+/// footnote covers and have no other assignment in the table; this reader fills all six with
+/// bullet, the codes remaining, in the footnote's own words, "subject to future
+/// reassignment". Footnotes 5 and 6 record that WinAnsiEncoding additionally encodes hyphen at
+/// 0xAD and space at 0xA0 (Windows Code Page 1252 reads those codes as soft hyphen and
+/// non-breaking space instead); this reader fills them with the plain hyphen and
+/// space names rather than the AGL's separate softhyphen/nonbreakingspace
+/// names, so a producer that means the distinct Unicode codepoint says so with its own
+/// /Differences entry, per the footnotes' own example.
+///
+/// src/VellumPdf.Conformance/Rules/Fonts/SimpleFontEncoding.cs carries its own copy of
+/// these three tables, deliberately left as it is (Conformance is Shipped and its verdicts are
+/// pinned against veraPDF). It diverges from the tables here at exactly
+/// eight WinAnsi codes (the six bullet fills above, plus 0xA0 and 0xAD, which that copy encodes
+/// under the AGL's own non-breaking-space/soft-hyphen names instead of the plain ones this reader
+/// uses) and seventeen MacRoman codes: fifteen where that copy carries a Mac OS Roman (1, 0)
+/// cmap-fallback glyph, from ISO 32000-2 Table 113 ("Additional entries in Mac OS Roman encoding
+/// not in MacRomanEncoding"), that Annex D.2 itself does not assign to MacRomanEncoding at all
+/// (notequal, infinity, lessequal, greaterequal, partialdiff,
+/// summation, product, pi, integral, Omega, radical,
+/// approxequal, Delta, lozenge and apple; §9.6.5.4 places Table 113 in
+/// the TrueType (1, 0) subtable fallback step, not in building the base encoding table), plus two
+/// codes where that copy's name disagrees with this class's own table: 0xCA (Annex D.2's own
+/// MacRoman column is blank there; footnote 6 assigns it space, "encoded as 312 (octal) in
+/// MacRomanEncoding", the same code WinAnsi's own footnote 6 dual-maps at 0xA0) and 0xDB (Annex
+/// D.2's own column reads currency; footnote 1: Apple's later Mac OS Roman revision
+/// reassigned that code to the Euro sign, but "this incompatible change has not been reflected in
+/// PDF's MacRomanEncoding, which continues to map code 333 to currency").
+///
+///
+internal static class SimpleFontEncodings
+{
+ private static readonly string?[] _standard = BuildStandard();
+ private static readonly string?[] _winAnsi = BuildWinAnsi();
+ private static readonly string?[] _macRoman = BuildMacRoman();
+ private static readonly string?[] _macExpert = new string?[256];
+
+ /// Adobe StandardEncoding (ISO 32000-2 Annex D.2), char code 0-255 to glyph
+ /// name.
+ public static ReadOnlySpan Standard => _standard;
+
+ /// WinAnsiEncoding (ISO 32000-2 Annex D.2, footnotes 3, 5 and 6 applied).
+ public static ReadOnlySpan WinAnsi => _winAnsi;
+
+ /// MacRomanEncoding (ISO 32000-2 Annex D.2).
+ public static ReadOnlySpan MacRoman => _macRoman;
+
+ ///
+ /// MacExpertEncoding: every cell is . Annex D.4 (Expert set and
+ /// MacExpert encoding) is not transcribed here: no oracle in this test suite exercises it, and
+ /// fonts that declare it are rare, so a font naming it gets the same outcome as a symbolic
+ /// font with no encoding: every code has no name, and text extraction reports no glyph for any
+ /// of them, rather than this reader refusing to recognise the name at all. Because the cells
+ /// are null for want of a transcription, not because Annex D.4 leaves them undefined,
+ /// does not run §9.6.5.4's StandardEncoding fill over a table
+ /// built from this one.
+ ///
+ public static ReadOnlySpan MacExpert => _macExpert;
+
+ ///
+ /// Resolves a /BaseEncoding or /Encoding name to its table. Recognises exactly
+ /// StandardEncoding, WinAnsiEncoding, MacRomanEncoding and
+ /// MacExpertEncoding; any other name, including a close variant, returns
+ /// . The returned span is backed by a shared static array and must be
+ /// copied (ToArray()) before a caller modifies a per-font table built from it.
+ ///
+ public static bool TryGetNamed(string name, out ReadOnlySpan table)
+ {
+ switch (name)
+ {
+ case "StandardEncoding": table = Standard; return true;
+ case "WinAnsiEncoding": table = WinAnsi; return true;
+ case "MacRomanEncoding": table = MacRoman; return true;
+ case "MacExpertEncoding": table = MacExpert; return true;
+ default: table = default; return false;
+ }
+ }
+
+ private static string?[] BuildStandard()
+ {
+ var t = new string?[256];
+ t[0x20] = "space"; t[0x21] = "exclam"; t[0x22] = "quotedbl"; t[0x23] = "numbersign";
+ t[0x24] = "dollar"; t[0x25] = "percent"; t[0x26] = "ampersand"; t[0x27] = "quoteright";
+ t[0x28] = "parenleft"; t[0x29] = "parenright"; t[0x2A] = "asterisk"; t[0x2B] = "plus";
+ t[0x2C] = "comma"; t[0x2D] = "hyphen"; t[0x2E] = "period"; t[0x2F] = "slash";
+ t[0x30] = "zero"; t[0x31] = "one"; t[0x32] = "two"; t[0x33] = "three";
+ t[0x34] = "four"; t[0x35] = "five"; t[0x36] = "six"; t[0x37] = "seven";
+ t[0x38] = "eight"; t[0x39] = "nine"; t[0x3A] = "colon"; t[0x3B] = "semicolon";
+ t[0x3C] = "less"; t[0x3D] = "equal"; t[0x3E] = "greater"; t[0x3F] = "question";
+ t[0x40] = "at";
+ t[0x41] = "A"; t[0x42] = "B"; t[0x43] = "C"; t[0x44] = "D"; t[0x45] = "E"; t[0x46] = "F";
+ t[0x47] = "G"; t[0x48] = "H"; t[0x49] = "I"; t[0x4A] = "J"; t[0x4B] = "K"; t[0x4C] = "L";
+ t[0x4D] = "M"; t[0x4E] = "N"; t[0x4F] = "O"; t[0x50] = "P"; t[0x51] = "Q"; t[0x52] = "R";
+ t[0x53] = "S"; t[0x54] = "T"; t[0x55] = "U"; t[0x56] = "V"; t[0x57] = "W"; t[0x58] = "X";
+ t[0x59] = "Y"; t[0x5A] = "Z";
+ t[0x5B] = "bracketleft"; t[0x5C] = "backslash"; t[0x5D] = "bracketright";
+ t[0x5E] = "asciicircum"; t[0x5F] = "underscore"; t[0x60] = "quoteleft";
+ t[0x61] = "a"; t[0x62] = "b"; t[0x63] = "c"; t[0x64] = "d"; t[0x65] = "e"; t[0x66] = "f";
+ t[0x67] = "g"; t[0x68] = "h"; t[0x69] = "i"; t[0x6A] = "j"; t[0x6B] = "k"; t[0x6C] = "l";
+ t[0x6D] = "m"; t[0x6E] = "n"; t[0x6F] = "o"; t[0x70] = "p"; t[0x71] = "q"; t[0x72] = "r";
+ t[0x73] = "s"; t[0x74] = "t"; t[0x75] = "u"; t[0x76] = "v"; t[0x77] = "w"; t[0x78] = "x";
+ t[0x79] = "y"; t[0x7A] = "z";
+ t[0x7B] = "braceleft"; t[0x7C] = "bar"; t[0x7D] = "braceright"; t[0x7E] = "asciitilde";
+ t[0xA1] = "exclamdown"; t[0xA2] = "cent"; t[0xA3] = "sterling"; t[0xA4] = "fraction";
+ t[0xA5] = "yen"; t[0xA6] = "florin"; t[0xA7] = "section"; t[0xA8] = "currency";
+ t[0xA9] = "quotesingle"; t[0xAA] = "quotedblleft"; t[0xAB] = "guillemotleft";
+ t[0xAC] = "guilsinglleft"; t[0xAD] = "guilsinglright"; t[0xAE] = "fi"; t[0xAF] = "fl";
+ t[0xB1] = "endash"; t[0xB2] = "dagger"; t[0xB3] = "daggerdbl"; t[0xB4] = "periodcentered";
+ t[0xB6] = "paragraph"; t[0xB7] = "bullet"; t[0xB8] = "quotesinglbase"; t[0xB9] = "quotedblbase";
+ t[0xBA] = "quotedblright"; t[0xBB] = "guillemotright"; t[0xBC] = "ellipsis";
+ t[0xBD] = "perthousand"; t[0xBF] = "questiondown";
+ t[0xC1] = "grave"; t[0xC2] = "acute"; t[0xC3] = "circumflex"; t[0xC4] = "tilde";
+ t[0xC5] = "macron"; t[0xC6] = "breve"; t[0xC7] = "dotaccent"; t[0xC8] = "dieresis";
+ t[0xCA] = "ring"; t[0xCB] = "cedilla"; t[0xCD] = "hungarumlaut"; t[0xCE] = "ogonek";
+ t[0xCF] = "caron"; t[0xD0] = "emdash";
+ t[0xE1] = "AE"; t[0xE3] = "ordfeminine"; t[0xE8] = "Lslash"; t[0xE9] = "Oslash";
+ t[0xEA] = "OE"; t[0xEB] = "ordmasculine"; t[0xF1] = "ae"; t[0xF5] = "dotlessi";
+ t[0xF8] = "lslash"; t[0xF9] = "oslash"; t[0xFA] = "oe"; t[0xFB] = "germandbls";
+ return t;
+ }
+
+ private static string?[] BuildWinAnsi()
+ {
+ var t = new string?[256];
+ t[0x20] = "space"; t[0x21] = "exclam"; t[0x22] = "quotedbl"; t[0x23] = "numbersign";
+ t[0x24] = "dollar"; t[0x25] = "percent"; t[0x26] = "ampersand"; t[0x27] = "quotesingle";
+ t[0x28] = "parenleft"; t[0x29] = "parenright"; t[0x2A] = "asterisk"; t[0x2B] = "plus";
+ t[0x2C] = "comma"; t[0x2D] = "hyphen"; t[0x2E] = "period"; t[0x2F] = "slash";
+ t[0x30] = "zero"; t[0x31] = "one"; t[0x32] = "two"; t[0x33] = "three";
+ t[0x34] = "four"; t[0x35] = "five"; t[0x36] = "six"; t[0x37] = "seven";
+ t[0x38] = "eight"; t[0x39] = "nine"; t[0x3A] = "colon"; t[0x3B] = "semicolon";
+ t[0x3C] = "less"; t[0x3D] = "equal"; t[0x3E] = "greater"; t[0x3F] = "question";
+ t[0x40] = "at";
+ t[0x41] = "A"; t[0x42] = "B"; t[0x43] = "C"; t[0x44] = "D"; t[0x45] = "E"; t[0x46] = "F";
+ t[0x47] = "G"; t[0x48] = "H"; t[0x49] = "I"; t[0x4A] = "J"; t[0x4B] = "K"; t[0x4C] = "L";
+ t[0x4D] = "M"; t[0x4E] = "N"; t[0x4F] = "O"; t[0x50] = "P"; t[0x51] = "Q"; t[0x52] = "R";
+ t[0x53] = "S"; t[0x54] = "T"; t[0x55] = "U"; t[0x56] = "V"; t[0x57] = "W"; t[0x58] = "X";
+ t[0x59] = "Y"; t[0x5A] = "Z";
+ t[0x5B] = "bracketleft"; t[0x5C] = "backslash"; t[0x5D] = "bracketright";
+ t[0x5E] = "asciicircum"; t[0x5F] = "underscore"; t[0x60] = "grave";
+ t[0x61] = "a"; t[0x62] = "b"; t[0x63] = "c"; t[0x64] = "d"; t[0x65] = "e"; t[0x66] = "f";
+ t[0x67] = "g"; t[0x68] = "h"; t[0x69] = "i"; t[0x6A] = "j"; t[0x6B] = "k"; t[0x6C] = "l";
+ t[0x6D] = "m"; t[0x6E] = "n"; t[0x6F] = "o"; t[0x70] = "p"; t[0x71] = "q"; t[0x72] = "r";
+ t[0x73] = "s"; t[0x74] = "t"; t[0x75] = "u"; t[0x76] = "v"; t[0x77] = "w"; t[0x78] = "x";
+ t[0x79] = "y"; t[0x7A] = "z";
+ t[0x7B] = "braceleft"; t[0x7C] = "bar"; t[0x7D] = "braceright"; t[0x7E] = "asciitilde";
+ // Footnote 3: the six codes this table would otherwise leave undefined between 0x20 and
+ // 0xFF, filled with bullet; see this class's own remarks for the footnote's exact words.
+ t[0x7F] = "bullet";
+ t[0x80] = "Euro"; t[0x81] = "bullet"; t[0x82] = "quotesinglbase"; t[0x83] = "florin";
+ t[0x84] = "quotedblbase"; t[0x85] = "ellipsis"; t[0x86] = "dagger";
+ t[0x87] = "daggerdbl"; t[0x88] = "circumflex"; t[0x89] = "perthousand";
+ t[0x8A] = "Scaron"; t[0x8B] = "guilsinglleft"; t[0x8C] = "OE";
+ t[0x8D] = "bullet"; t[0x8E] = "Zcaron"; t[0x8F] = "bullet";
+ t[0x90] = "bullet"; t[0x91] = "quoteleft"; t[0x92] = "quoteright"; t[0x93] = "quotedblleft";
+ t[0x94] = "quotedblright"; t[0x95] = "bullet"; t[0x96] = "endash";
+ t[0x97] = "emdash"; t[0x98] = "tilde"; t[0x99] = "trademark";
+ t[0x9A] = "scaron"; t[0x9B] = "guilsinglright"; t[0x9C] = "oe";
+ t[0x9D] = "bullet"; t[0x9E] = "zcaron"; t[0x9F] = "Ydieresis";
+ // Footnotes 5 and 6: the dual mapping described in this class's own remarks.
+ t[0xA0] = "space";
+ t[0xA1] = "exclamdown"; t[0xA2] = "cent"; t[0xA3] = "sterling";
+ t[0xA4] = "currency"; t[0xA5] = "yen"; t[0xA6] = "brokenbar"; t[0xA7] = "section";
+ t[0xA8] = "dieresis"; t[0xA9] = "copyright"; t[0xAA] = "ordfeminine"; t[0xAB] = "guillemotleft";
+ t[0xAC] = "logicalnot"; t[0xAD] = "hyphen"; t[0xAE] = "registered"; t[0xAF] = "macron";
+ t[0xB0] = "degree"; t[0xB1] = "plusminus"; t[0xB2] = "twosuperior"; t[0xB3] = "threesuperior";
+ t[0xB4] = "acute"; t[0xB5] = "mu"; t[0xB6] = "paragraph"; t[0xB7] = "periodcentered";
+ t[0xB8] = "cedilla"; t[0xB9] = "onesuperior"; t[0xBA] = "ordmasculine"; t[0xBB] = "guillemotright";
+ t[0xBC] = "onequarter"; t[0xBD] = "onehalf"; t[0xBE] = "threequarters"; t[0xBF] = "questiondown";
+ t[0xC0] = "Agrave"; t[0xC1] = "Aacute"; t[0xC2] = "Acircumflex"; t[0xC3] = "Atilde";
+ t[0xC4] = "Adieresis"; t[0xC5] = "Aring"; t[0xC6] = "AE"; t[0xC7] = "Ccedilla";
+ t[0xC8] = "Egrave"; t[0xC9] = "Eacute"; t[0xCA] = "Ecircumflex"; t[0xCB] = "Edieresis";
+ t[0xCC] = "Igrave"; t[0xCD] = "Iacute"; t[0xCE] = "Icircumflex"; t[0xCF] = "Idieresis";
+ t[0xD0] = "Eth"; t[0xD1] = "Ntilde"; t[0xD2] = "Ograve"; t[0xD3] = "Oacute";
+ t[0xD4] = "Ocircumflex"; t[0xD5] = "Otilde"; t[0xD6] = "Odieresis"; t[0xD7] = "multiply";
+ t[0xD8] = "Oslash"; t[0xD9] = "Ugrave"; t[0xDA] = "Uacute"; t[0xDB] = "Ucircumflex";
+ t[0xDC] = "Udieresis"; t[0xDD] = "Yacute"; t[0xDE] = "Thorn"; t[0xDF] = "germandbls";
+ t[0xE0] = "agrave"; t[0xE1] = "aacute"; t[0xE2] = "acircumflex"; t[0xE3] = "atilde";
+ t[0xE4] = "adieresis"; t[0xE5] = "aring"; t[0xE6] = "ae"; t[0xE7] = "ccedilla";
+ t[0xE8] = "egrave"; t[0xE9] = "eacute"; t[0xEA] = "ecircumflex"; t[0xEB] = "edieresis";
+ t[0xEC] = "igrave"; t[0xED] = "iacute"; t[0xEE] = "icircumflex"; t[0xEF] = "idieresis";
+ t[0xF0] = "eth"; t[0xF1] = "ntilde"; t[0xF2] = "ograve"; t[0xF3] = "oacute";
+ t[0xF4] = "ocircumflex"; t[0xF5] = "otilde"; t[0xF6] = "odieresis"; t[0xF7] = "divide";
+ t[0xF8] = "oslash"; t[0xF9] = "ugrave"; t[0xFA] = "uacute"; t[0xFB] = "ucircumflex";
+ t[0xFC] = "udieresis"; t[0xFD] = "yacute"; t[0xFE] = "thorn"; t[0xFF] = "ydieresis";
+ return t;
+ }
+
+ private static string?[] BuildMacRoman()
+ {
+ var t = new string?[256];
+ t[0x20] = "space"; t[0x21] = "exclam"; t[0x22] = "quotedbl"; t[0x23] = "numbersign";
+ t[0x24] = "dollar"; t[0x25] = "percent"; t[0x26] = "ampersand"; t[0x27] = "quotesingle";
+ t[0x28] = "parenleft"; t[0x29] = "parenright"; t[0x2A] = "asterisk"; t[0x2B] = "plus";
+ t[0x2C] = "comma"; t[0x2D] = "hyphen"; t[0x2E] = "period"; t[0x2F] = "slash";
+ t[0x30] = "zero"; t[0x31] = "one"; t[0x32] = "two"; t[0x33] = "three";
+ t[0x34] = "four"; t[0x35] = "five"; t[0x36] = "six"; t[0x37] = "seven";
+ t[0x38] = "eight"; t[0x39] = "nine"; t[0x3A] = "colon"; t[0x3B] = "semicolon";
+ t[0x3C] = "less"; t[0x3D] = "equal"; t[0x3E] = "greater"; t[0x3F] = "question";
+ t[0x40] = "at";
+ t[0x41] = "A"; t[0x42] = "B"; t[0x43] = "C"; t[0x44] = "D"; t[0x45] = "E"; t[0x46] = "F";
+ t[0x47] = "G"; t[0x48] = "H"; t[0x49] = "I"; t[0x4A] = "J"; t[0x4B] = "K"; t[0x4C] = "L";
+ t[0x4D] = "M"; t[0x4E] = "N"; t[0x4F] = "O"; t[0x50] = "P"; t[0x51] = "Q"; t[0x52] = "R";
+ t[0x53] = "S"; t[0x54] = "T"; t[0x55] = "U"; t[0x56] = "V"; t[0x57] = "W"; t[0x58] = "X";
+ t[0x59] = "Y"; t[0x5A] = "Z";
+ t[0x5B] = "bracketleft"; t[0x5C] = "backslash"; t[0x5D] = "bracketright";
+ t[0x5E] = "asciicircum"; t[0x5F] = "underscore"; t[0x60] = "grave";
+ t[0x61] = "a"; t[0x62] = "b"; t[0x63] = "c"; t[0x64] = "d"; t[0x65] = "e"; t[0x66] = "f";
+ t[0x67] = "g"; t[0x68] = "h"; t[0x69] = "i"; t[0x6A] = "j"; t[0x6B] = "k"; t[0x6C] = "l";
+ t[0x6D] = "m"; t[0x6E] = "n"; t[0x6F] = "o"; t[0x70] = "p"; t[0x71] = "q"; t[0x72] = "r";
+ t[0x73] = "s"; t[0x74] = "t"; t[0x75] = "u"; t[0x76] = "v"; t[0x77] = "w"; t[0x78] = "x";
+ t[0x79] = "y"; t[0x7A] = "z";
+ t[0x7B] = "braceleft"; t[0x7C] = "bar"; t[0x7D] = "braceright"; t[0x7E] = "asciitilde";
+ t[0x80] = "Adieresis"; t[0x81] = "Aring"; t[0x82] = "Ccedilla"; t[0x83] = "Eacute";
+ t[0x84] = "Ntilde"; t[0x85] = "Odieresis"; t[0x86] = "Udieresis"; t[0x87] = "aacute";
+ t[0x88] = "agrave"; t[0x89] = "acircumflex"; t[0x8A] = "adieresis"; t[0x8B] = "atilde";
+ t[0x8C] = "aring"; t[0x8D] = "ccedilla"; t[0x8E] = "eacute"; t[0x8F] = "egrave";
+ t[0x90] = "ecircumflex"; t[0x91] = "edieresis"; t[0x92] = "iacute"; t[0x93] = "igrave";
+ t[0x94] = "icircumflex"; t[0x95] = "idieresis"; t[0x96] = "ntilde"; t[0x97] = "oacute";
+ t[0x98] = "ograve"; t[0x99] = "ocircumflex"; t[0x9A] = "odieresis"; t[0x9B] = "otilde";
+ t[0x9C] = "uacute"; t[0x9D] = "ugrave"; t[0x9E] = "ucircumflex"; t[0x9F] = "udieresis";
+ t[0xA0] = "dagger"; t[0xA1] = "degree"; t[0xA2] = "cent"; t[0xA3] = "sterling";
+ t[0xA4] = "section"; t[0xA5] = "bullet"; t[0xA6] = "paragraph"; t[0xA7] = "germandbls";
+ t[0xA8] = "registered"; t[0xA9] = "copyright"; t[0xAA] = "trademark"; t[0xAB] = "acute";
+ t[0xAC] = "dieresis";
+ // 0xAD ("notequal" in Mac OS Roman's own charset) is not one of Annex D.2's MacRoman
+ // cells (see this class's own remarks), so this reader leaves it undefined.
+ t[0xAE] = "AE"; t[0xAF] = "Oslash";
+ // 0xB0, 0xB2, 0xB3, 0xB6-0xBA and 0xBD (Mac OS Roman's own math/symbol glyphs) are the
+ // same kind of Table 113 cell as 0xAD above; left undefined for the same reason.
+ t[0xB1] = "plusminus";
+ t[0xB4] = "yen"; t[0xB5] = "mu";
+ t[0xBB] = "ordfeminine";
+ t[0xBC] = "ordmasculine"; t[0xBE] = "ae"; t[0xBF] = "oslash";
+ t[0xC0] = "questiondown"; t[0xC1] = "exclamdown"; t[0xC2] = "logicalnot";
+ // 0xC3, 0xC5, 0xC6 (radical, approxequal, Delta): Table 113 cells, not Annex D.2 ones.
+ t[0xC4] = "florin";
+ t[0xC7] = "guillemotleft";
+ t[0xC8] = "guillemotright"; t[0xC9] = "ellipsis";
+ // Footnote 6's dual mapping (see this class's own remarks): plain "space", not the AGL's
+ // separate "nonbreakingspace" name.
+ t[0xCA] = "space";
+ t[0xCB] = "Agrave";
+ t[0xCC] = "Atilde"; t[0xCD] = "Otilde"; t[0xCE] = "OE"; t[0xCF] = "oe";
+ t[0xD0] = "endash"; t[0xD1] = "emdash"; t[0xD2] = "quotedblleft"; t[0xD3] = "quotedblright";
+ t[0xD4] = "quoteleft"; t[0xD5] = "quoteright"; t[0xD6] = "divide";
+ // 0xD7 (lozenge): a Table 113 cell, not an Annex D.2 one.
+ t[0xD8] = "ydieresis"; t[0xD9] = "Ydieresis"; t[0xDA] = "fraction";
+ // Footnote 1: Annex D.2 and its own text both read "currency" at this code; Apple's later
+ // Mac OS Roman revision reassigned it to the Euro sign, but PDF's MacRomanEncoding does
+ // not follow that change (see this class's own remarks for the footnote's exact words).
+ t[0xDB] = "currency";
+ t[0xDC] = "guilsinglleft"; t[0xDD] = "guilsinglright"; t[0xDE] = "fi"; t[0xDF] = "fl";
+ t[0xE0] = "daggerdbl"; t[0xE1] = "periodcentered"; t[0xE2] = "quotesinglbase"; t[0xE3] = "quotedblbase";
+ t[0xE4] = "perthousand"; t[0xE5] = "Acircumflex"; t[0xE6] = "Ecircumflex"; t[0xE7] = "Aacute";
+ t[0xE8] = "Edieresis"; t[0xE9] = "Egrave"; t[0xEA] = "Iacute"; t[0xEB] = "Icircumflex";
+ t[0xEC] = "Idieresis"; t[0xED] = "Igrave"; t[0xEE] = "Oacute"; t[0xEF] = "Ocircumflex";
+ // 0xF0 (apple): the Mac OS Apple-logo glyph, a Table 113 cell, not an Annex D.2 one.
+ t[0xF1] = "Ograve"; t[0xF2] = "Uacute"; t[0xF3] = "Ucircumflex";
+ t[0xF4] = "Ugrave"; t[0xF5] = "dotlessi"; t[0xF6] = "circumflex"; t[0xF7] = "tilde";
+ t[0xF8] = "macron"; t[0xF9] = "breve"; t[0xFA] = "dotaccent"; t[0xFB] = "ring";
+ t[0xFC] = "cedilla"; t[0xFD] = "hungarumlaut"; t[0xFE] = "ogonek"; t[0xFF] = "caron";
+ return t;
+ }
+}
diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs
new file mode 100644
index 00000000..ddca6aa0
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs
@@ -0,0 +1,614 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Globalization;
+using VellumPdf.Core;
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// Decodes a Type1, MMType1 or TrueType simple font (ISO 32000-2 §9.6.5): resolves the font's
+/// character-code-to-glyph-name table from its /Encoding, then to Unicode through the Adobe
+/// Glyph List, and its per-code advance widths from /Widths or, for a standard 14 font with
+/// none, ' own name-keyed AFM widths.
+///
+///
+/// §9.6.5.4 gives TrueType fonts their own base-encoding rule, distinct from Type1's (§9.6.5.2):
+/// initialise from the named encoding or /Differences' own /BaseEncoding, then fill
+/// anything still undefined from StandardEncoding. This reader applies that same rule uniformly to
+/// Type1, MMType1 and TrueType alike, rather than branching by subtype, since without parsing the
+/// font program itself there is no way to tell a Type1 font's built-in encoding from a TrueType
+/// one's: both are unavailable data, and the two subclauses converge on the same practical
+/// fallback (StandardEncoding for a nonsymbolic font) wherever the font program would
+/// otherwise supply an answer this reader cannot. The one step that does branch on the subtype
+/// needs no font program: §9.6.5.4's closing rule for a TrueType font whose /Encoding is a
+/// dictionary, "Finally, any undefined entries in the table shall be filled using
+/// StandardEncoding", is applied after /Differences. The clause's own precondition for
+/// building this table at all names "the font descriptor's Nonsymbolic flag" of Table 121, a
+/// condition on a descriptor that is present, so this reader runs the fill only when
+/// /FontDescriptor and its /Flags both exist. The Table 112 default elsewhere in this
+/// class treats a missing descriptor as nonsymbolic, and that default is not extended here: Table
+/// 109 makes /FontDescriptor required, optional only in PDF 1.0 to 1.7 for the standard 14,
+/// so its absence on any other font is a producer defect, and running a fill the clause conditions
+/// on the descriptor would be papering over it. Which flag decides the state follows §9.8.2, "A PDF
+/// processor should always check the Symbolic flag to determine whether the state is Symbolic or
+/// NonSymbolic": the fill runs when the Symbolic flag is clear. Table 121 requires the two flags to
+/// be complementary ("This flag and the Nonsymbolic flag shall not both be set or both be clear"),
+/// so the two readings agree on every conformant descriptor and differ only on one whose flags
+/// disagree, where the Symbolic flag wins here as it does when the constructor classifies the
+/// font at step 3. The only cells the
+/// fill can change are the twelve StandardEncoding cells MacRomanEncoding leaves undefined
+/// (SimpleFontEncodingsTests pins the set; WinAnsiEncoding leaves none, and a dictionary
+/// without /BaseEncoding starts from StandardEncoding already), fewer when
+/// /Differences has named one of them, and it is skipped for a
+/// /BaseEncoding /MacExpertEncoding, whose table this reader carries as all-null
+/// (), so "undefined" cannot be told from "not
+/// transcribed". §9.6.5.2 states no such rule for Type1 fonts, and none is applied.
+///
+/// Every dictionary entry this class reads, wherever it is read, goes through this class's own
+/// Resolve helper before its type is tested (one hop through
+/// , with a dangling reference or a resolved
+/// , direct or reached through that hop, normalised to
+/// and treated as absent per §7.3.7), with one exception:
+/// /ToUnicode, whose reference is followed with
+/// because Resolve
+/// hands back a stream object's dictionary and never the itself. An
+/// element of /Differences is read raw as well (§9.6.5.1, in ApplyDifferences), but
+/// that is an array element rather than a dictionary entry: §7.3.10 permits an indirect reference
+/// there and this reader deliberately does not resolve one, recording that as a reader limitation
+/// () rather than silently supporting
+/// or silently rejecting it.
+///
+///
+/// A /Differences name longer than reports
+/// that same reader limitation and leaves the code undefined, rather than keeping whatever name
+/// the base encoding had assigned there: the oversized name still occupies its slot in the
+/// sequence (the running code still advances past it), so refusing to record it erases the base
+/// encoding's own glyph at that code, it does not preserve it.
+///
+///
+/// ISO 32000-2 §9.6.5.1 states the ordering rule for /Differences sequences verbatim: "These
+/// sequences may be specified in any order but shall not overlap." This reader does not enforce
+/// that rule: two sequences that assign the same code are applied in array order, so a later one
+/// silently overwrites an earlier one's name there, with no diagnostic for the overlap itself.
+/// The same clause says "Each code shall be the first index in a sequence of character codes to be
+/// changed", so an array opening with a name has no code to start from; this reader starts it at
+/// code 0 and reports nothing, on the same reasoning as the overlap: applying the names a producer
+/// wrote loses less than discarding the whole array over its missing first element.
+///
+///
+/// §9.6.5.4 also states, verbatim: "When the font has no Encoding entry, or the font descriptor's
+/// Symbolic flag is set (in which case the Encoding entry is ignored), this shall occur: ..." (the
+/// steps that follow need a (3, 0) or (1, 0) cmap subtable from the font program, which this
+/// reader does not read). For a TrueType font whose descriptor sets the Symbolic flag but which
+/// still carries a dictionary /Encoding, this reader honours the entry rather than ignoring
+/// it: the clause's own alternative needs font-program data this reader has no access to, and the
+/// clause's preceding paragraph already readmits a symbolic font that names MacRomanEncoding or
+/// WinAnsiEncoding, so following a dictionary the file went to the trouble of writing is closer to
+/// what the font program would have produced than discarding it.
+///
+///
+internal sealed class SimpleFontReader : PdfFontReader
+{
+ private static readonly PdfName _fontDescriptorKey = new("FontDescriptor");
+ private static readonly PdfName _flagsKey = new("Flags");
+ private static readonly PdfName _baseEncodingKey = new("BaseEncoding");
+ private static readonly PdfName _differencesKey = new("Differences");
+ private static readonly PdfName _firstCharKey = new("FirstChar");
+ private static readonly PdfName _lastCharKey = new("LastChar");
+ private static readonly PdfName _widthsKey = new("Widths");
+ private static readonly PdfName _missingWidthKey = new("MissingWidth");
+ private static readonly PdfName _toUnicodeKey = new("ToUnicode");
+
+ private const int SymbolicFlagBit = 4; // bit position 3 (ISO 32000-2 Table 121): value 2^(3-1).
+
+ private readonly DiagnosticSink _sink;
+ private readonly int? _objectNumber;
+ private readonly int? _generation;
+ private readonly int? _pageIndex;
+
+ // Empty placeholders: Populate (or Create's own catch block) always replaces all three with a
+ // freshly built 256-element array before any caller can observe them, so allocating that size
+ // here too would be a second, wasted allocation per font.
+ private string?[] _names = [];
+ private double[] _widths = [];
+ private string?[] _unicode = [];
+ private bool _hasToUnicode;
+ private bool _hasAnyMappedCode;
+
+ private bool _reported400;
+ private bool _reported401;
+ private bool _reported402;
+ private bool _reportedNoUnicodeOrUnmapped;
+
+ private SimpleFontReader(DiagnosticSink sink, int? objectNumber, int? generation, int? pageIndex)
+ {
+ _sink = sink;
+ _objectNumber = objectNumber;
+ _generation = generation;
+ _pageIndex = pageIndex;
+ }
+
+ ///
+ public override bool HasToUnicode => _hasToUnicode;
+
+ ///
+ /// Builds a reader for a Type1, MMType1 or TrueType font dictionary.
+ /// resolves indirect references (see this class's own remarks);
+ /// and , when the font dictionary itself was reached through one,
+ /// and are attached to every diagnostic this build reports.
+ ///
+ internal static SimpleFontReader Create(
+ PdfDocumentReader reader, PdfDictionary fontDict, int? objectNumber, int? generation,
+ DiagnosticSink sink, int? pageIndex)
+ {
+ var self = new SimpleFontReader(sink, objectNumber, generation, pageIndex);
+ try
+ {
+ self.Populate(reader, fontDict);
+ }
+ catch (InvalidDataException)
+ {
+ // reader.Resolve throws past MaxResolveDepth (PdfDocumentReader.cs). FontFuzzTests
+ // covers a wide range of malformed dictionaries (subtypes, /Encoding shapes including
+ // an indirect chain, /Differences, /Widths, /ToUnicode shapes) without reaching this
+ // catch; it is not itself the proof this clause is the only thing Populate can throw.
+ // GetFontReader's own dedicated regression test drives a MaxResolveDepth chain through
+ // a font entry instead, since building one needs an object graph parsed from bytes,
+ // not a fuzzed in-memory dictionary.
+ self._names = new string?[256];
+ self._widths = new double[256];
+ self._unicode = new string?[256];
+ self._hasToUnicode = false;
+ self._hasAnyMappedCode = false;
+ self.ReportOnce(ref self._reported400, PdfReaderDiagnosticCode.FontUnreadable,
+ "building this font hit the reader's own indirect-object resolution depth limit.");
+ }
+ return self;
+ }
+
+ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict)
+ {
+ // Step 2: /BaseFont. A name longer than Standard14Names could ever resolve is no more
+ // usable than a missing or wrong-typed one: it never selects a standard 14 font, and
+ // quoting it whole in a diagnostic would be the unbounded-allocation risk
+ // DiagnosticExcerpt exists to avoid, so both report the same 400 message.
+ var baseFontResolved = Resolve(reader, fontDict.Get(PdfName.BaseFont));
+ string? afmName = null;
+ if (baseFontResolved is PdfName baseFontName && baseFontName.Value.Length <= AdobeGlyphList.MaxGlyphNameLength)
+ {
+ Standard14Names.TryResolve(baseFontName.Value, out var resolved);
+ afmName = resolved.Length == 0 ? null : resolved;
+ }
+ else
+ {
+ var excerpt = baseFontResolved is PdfName oversized
+ ? DiagnosticExcerpt.Quote(oversized.Value)
+ : "(not a name)";
+ ReportOnce(ref _reported400, PdfReaderDiagnosticCode.FontUnreadable,
+ $"has no usable /BaseFont: {excerpt}.");
+ }
+
+ // Step 3: symbolic. A /Flags of the wrong type (Table 120 types the entry as an integer
+ // and §7.3.3 forbids a real where an integer is expected; a real is the one a producer is
+ // most likely to write by mistake) is treated the same as an absent one, silently: of this
+ // class's five diagnostic codes, 400 to 402 describe the font dictionary and 403/404 its
+ // Unicode routing, so none fits a malformed descriptor entry, and no sixth is added, since
+ // the Table 112 default below leaves the font in a usable state either way. What the
+ // producer loses is step 5's §9.6.5.4 fill: on a nonsymbolic TrueType with a dictionary
+ // /Encoding, a /Flags the reader cannot read leaves the twelve StandardEncoding cells
+ // undefined that a readable one would have filled.
+ var descriptor = Resolve(reader, fontDict.Get(_fontDescriptorKey)) as PdfDictionary;
+ var resolvedFlags = descriptor is not null
+ ? Resolve(reader, descriptor.Get(_flagsKey)) as PdfInteger
+ : null;
+ bool symbolic;
+ if (resolvedFlags is not null)
+ {
+ symbolic = (resolvedFlags.Value & SymbolicFlagBit) != 0;
+ }
+ else
+ {
+ symbolic = afmName is "Symbol" or "ZapfDingbats";
+ }
+
+ // §9.6.5.4's own StandardEncoding fill (step 5) is conditioned on "the font descriptor's
+ // Nonsymbolic flag ... is set" (see the class remarks): a condition on a present
+ // descriptor, unlike the Table 112 default above, which treats a missing descriptor as
+ // nonsymbolic. Table 109 makes /FontDescriptor required, optional only in PDF 1.0 to 1.7
+ // for the standard 14, so its absence on any other font is a producer defect this reader
+ // does not paper over by running the fill. With a descriptor present, the state is read
+ // from the Symbolic flag, as step 3 read it, per §9.8.2's "A PDF processor should always
+ // check the Symbolic flag"; a descriptor whose two flags disagree is not read differently
+ // here than there.
+ var descriptorNonsymbolic = resolvedFlags is not null && !symbolic;
+
+ // Step 4: base table, then /Differences. Symbol and ZapfDingbats get no special path
+ // here: their built-in encodings are the Table 112 default base encoding (the "font's
+ // built-in encoding" case), and §9.6.5.2 says an /Encoding entry, "if present, shall
+ // override a Type 1 font's mapping from character codes to character names", so a named
+ // /Encoding or a /Differences array applies to them exactly as to any other font.
+ var table = ResolveEncodingTable(
+ reader, fontDict, symbolic, afmName, out var encodingDict, out var standardFillAllowed);
+ if (encodingDict is not null)
+ {
+ ApplyDifferences(reader, encodingDict, table);
+
+ // Step 5: §9.6.5.4's closing rule (see the class remarks for its exact scope and for
+ // why this needs a present descriptor rather than "!symbolic" alone).
+ var trueType = Resolve(reader, fontDict.Get(PdfName.Subtype)) is PdfName { Value: "TrueType" };
+ if (trueType && descriptorNonsymbolic && standardFillAllowed)
+ FillUndefinedFromStandard(table);
+ }
+
+ _names = table;
+
+ // Step 6/7: widths.
+ var descriptorMissingWidth = 0.0;
+ if (descriptor is not null && Resolve(reader, descriptor.Get(_missingWidthKey)) is { } mw)
+ {
+ descriptorMissingWidth = mw switch { PdfInteger i => i.Value, PdfReal r => r.Value, _ => 0.0 };
+ }
+
+ var widths = new double[256];
+ Array.Fill(widths, descriptorMissingWidth);
+ var usesAfmWidths = BuildWidths(reader, fontDict, widths, afmName);
+ _widths = widths;
+
+ // Step 8: Unicode, per code, from the glyph name.
+ var unicode = new string?[256];
+ var zapf = afmName == "ZapfDingbats";
+ for (var code = 0; code < 256; code++)
+ {
+ var name = table[code];
+ if (name is null)
+ continue;
+
+ if (zapf && ZapfDingbatsGlyphList.TryMap(name, out var zapfUnicode))
+ unicode[code] = zapfUnicode;
+ else if (AdobeGlyphList.TryMapToUnicode(name, out var aglUnicode))
+ unicode[code] = aglUnicode;
+ }
+ _unicode = unicode;
+ _hasAnyMappedCode = Array.Exists(unicode, u => u is not null);
+
+ // Step 9: AFM widths, only when /Widths itself was absent (step 7 deferred this here,
+ // since a text font's width needs this step's own Unicode table).
+ if (usesAfmWidths)
+ FillAfmWidths(afmName!, table, widths, descriptorMissingWidth);
+
+ // /ToUnicode: recorded only, not parsed yet (see PdfFontReader's doc). A stream object is
+ // always indirect (§7.3.8.1), and PdfDocumentReader.Resolve hands back a stream object's
+ // dictionary rather than a stream, so the reference is followed with ResolveStream; the
+ // direct PdfStream arm serves dictionaries built in memory, which a parsed file never
+ // produces.
+ _hasToUnicode = fontDict.Get(_toUnicodeKey) switch
+ {
+ PdfIndirectReference toUnicodeRef => reader.ResolveStream(toUnicodeRef) is not null,
+ PdfStream => true,
+ _ => false,
+ };
+
+ // Step 10: 403, reported once, right here; 404 is decided lazily in TryDecodeNext, using
+ // _hasAnyMappedCode computed above so that check costs nothing per decoded byte.
+ if (!_hasToUnicode && !_hasAnyMappedCode)
+ {
+ ReportOnce(ref _reportedNoUnicodeOrUnmapped, PdfReaderDiagnosticCode.FontNoUnicodeRoute,
+ "no code in this font has a route to Unicode: no /ToUnicode stream, and no glyph "
+ + "name the Adobe Glyph List (or the ZapfDingbats list) maps.");
+ }
+ }
+
+ // standardFillAllowed is true for an encoding dictionary whose base table is one this reader
+ // transcribes in full, so that its null cells are the "undefined entries" §9.6.5.4 speaks of;
+ // it is false for a name /Encoding (the clause's fill belongs to the dictionary case only) and
+ // for a /BaseEncoding /MacExpertEncoding (see the class remarks).
+ private string?[] ResolveEncodingTable(
+ PdfDocumentReader reader, PdfDictionary fontDict, bool symbolic, string? afmName,
+ out PdfDictionary? encodingDict, out bool standardFillAllowed)
+ {
+ encodingDict = null;
+ standardFillAllowed = false;
+ var encoding = Resolve(reader, fontDict.Get(PdfName.Encoding));
+ switch (encoding)
+ {
+ case null:
+ return TableDefault(symbolic, afmName);
+
+ case PdfName named:
+ if (SimpleFontEncodings.TryGetNamed(named.Value, out var byName))
+ return byName.ToArray();
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ $"/Encoding names an encoding this reader does not know: "
+ + $"{DiagnosticExcerpt.Quote(named.Value)}.");
+ return TableDefault(symbolic, afmName);
+
+ case PdfDictionary dict:
+ encodingDict = dict;
+ standardFillAllowed = true;
+ var baseEncoding = Resolve(reader, dict.Get(_baseEncodingKey));
+ if (baseEncoding is null)
+ return TableDefault(symbolic, afmName);
+ if (baseEncoding is PdfName baseName && SimpleFontEncodings.TryGetNamed(baseName.Value, out var baseTable))
+ {
+ standardFillAllowed = baseName.Value != "MacExpertEncoding";
+ return baseTable.ToArray();
+ }
+ if (baseEncoding is PdfIndirectReference)
+ {
+ // A reference here has already been through one Resolve hop and is STILL a
+ // reference: a second link in the chain, which this reader does not follow
+ // (see the class remarks). Naming it "an encoding this reader does not know"
+ // would be wrong: it names no encoding at all, resolved or not.
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ "/Encoding's /BaseEncoding is an indirect reference this reader does not "
+ + "follow past one hop.");
+ return TableDefault(symbolic, afmName);
+ }
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ "/Encoding's /BaseEncoding names an encoding this reader does not know.");
+ return TableDefault(symbolic, afmName);
+
+ default:
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ "/Encoding is neither a known encoding name nor an encoding dictionary.");
+ return TableDefault(symbolic, afmName);
+ }
+ }
+
+ // Table 112's default base encoding is the font program's built-in encoding for an embedded
+ // font or a symbolic one, and StandardEncoding for a nonsymbolic one. This reader never
+ // parses a font program, so the built-in encoding is known only for the two standard 14
+ // fonts Annex D.5 and D.6 print it for; every other symbolic font gets an all-null table,
+ // and whether the font is embedded plays no part.
+ private static string?[] TableDefault(bool symbolic, string? afmName) => afmName switch
+ {
+ "Symbol" => SymbolFontMetrics.SymbolEncoding.ToArray(),
+ "ZapfDingbats" => SymbolFontMetrics.ZapfDingbatsEncoding.ToArray(),
+ _ => symbolic ? new string?[256] : SimpleFontEncodings.Standard.ToArray(),
+ };
+
+ private static void FillUndefinedFromStandard(string?[] table)
+ {
+ var standard = SimpleFontEncodings.Standard;
+ for (var code = 0; code < 256; code++)
+ table[code] ??= standard[code];
+ }
+
+ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDict, string?[] table)
+ {
+ var resolved = Resolve(reader, encodingDict.Get(_differencesKey));
+ if (resolved is null)
+ return; // absent (see this class's Resolve helper): omitted, null, or a dangling reference.
+
+ if (resolved is not PdfArray differences)
+ {
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ $"/Differences is present but not an array: {DescribeNonArrayType(resolved)}.");
+ return;
+ }
+
+ var code = 0;
+ for (var i = 0; i < differences.Count; i++)
+ {
+ // Raw, not resolved: §7.3.10 permits an indirect reference here, and this reader
+ // deliberately does not follow one; see the class doc's own remarks.
+ var element = differences[i];
+ switch (element)
+ {
+ case PdfInteger codeInt:
+ if (codeInt.Value is < 0 or > 255)
+ {
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ $"/Differences sets the current code to {codeInt.Value}, outside 0..255.");
+ return; // the rest of the array is ignored.
+ }
+ code = (int)codeInt.Value;
+ break;
+
+ case PdfName glyphName:
+ if (code > 255)
+ {
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ "/Differences assigns a name past code 255.");
+ return; // stop.
+ }
+ if (glyphName.Value.Length > AdobeGlyphList.MaxGlyphNameLength)
+ {
+ // The flag is tested before the message is built, not just inside
+ // ReportOnce, because this is the one Report call in this class reachable
+ // from an unbounded loop: building the interpolated message and the
+ // DiagnosticExcerpt.Quote call for every oversized element, only to have
+ // ReportOnce discard all but the first, is an allocation a 100,000-element
+ // array should not have to pay for.
+ if (!_reported401)
+ {
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ $"/Differences names a glyph longer than {AdobeGlyphList.MaxGlyphNameLength} characters: "
+ + $"{DiagnosticExcerpt.Quote(glyphName.Value)}.");
+ }
+ table[code] = null; // the code stays undefined; see this class's own doc.
+ }
+ else
+ {
+ // A later assignment overwrites an earlier one at the same code. ISO
+ // 32000-2 §9.6.5.1 forbids overlapping sequences; this reader allows
+ // the overwrite and reports nothing for it (see the class remarks).
+ table[code] = glyphName.Value;
+ }
+ code++;
+ break;
+
+ default:
+ // §9.6.5.1 permits only integers and names in this array; a direct real,
+ // string, boolean, dictionary or nested array is not "unresolved", it is of a
+ // type the clause does not permit there. An indirect reference is the one
+ // shape §7.3.10 does permit, that this reader still does not follow.
+ var kind = element is PdfIndirectReference
+ ? "an indirect reference, which this reader does not follow inside /Differences"
+ : $"an element that is neither an integer nor a name ({DescribeNonArrayType(element)})";
+ ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed,
+ $"/Differences contains {kind}.");
+ // Stop applying the array at the first element this reader cannot interpret,
+ // rather than resuming after it with the running code unchanged: that
+ // resumption is what let a later name silently overwrite an earlier one's
+ // code.
+ return;
+ }
+ }
+ }
+
+ // Names a resolved value's type for the 401 message reported when /Differences is present but
+ // not an array, or contains an element of a type §9.6.5.1 does not permit there: a name or
+ // keyword goes through DiagnosticExcerpt, matching every other Report call in this class.
+ private static string DescribeNonArrayType(PdfObject value) => value switch
+ {
+ PdfDictionary => "a dictionary",
+ PdfIndirectReference => "an indirect reference this reader does not follow past one hop",
+ PdfName n => $"the name {DiagnosticExcerpt.Quote(n.Value)}",
+ PdfInteger i => $"the integer {i.Value}",
+ // Invariant so the message does not change with the host culture's decimal separator.
+ PdfReal r => $"the real number {r.Value.ToString(CultureInfo.InvariantCulture)}",
+ PdfBoolean b => $"the boolean {(b.Value ? "true" : "false")}",
+ PdfLiteralString or PdfHexString => "a string",
+ PdfArray => "a nested array",
+ PdfNull => "a null",
+ PdfStream => "a stream",
+ _ => "a value of a type this reader does not recognise",
+ };
+
+ /// Returns when /Widths was absent, meaning step 9's AFM fill
+ /// applies (only for a standard 14 or aliased font; any other font keeps MissingWidth
+ /// everywhere and reports 402).
+ private bool BuildWidths(PdfDocumentReader reader, PdfDictionary fontDict, double[] widths, string? afmName)
+ {
+ var widthsResolved = Resolve(reader, fontDict.Get(_widthsKey));
+ if (widthsResolved is null)
+ {
+ if (afmName is not null)
+ return true;
+
+ ReportOnce(ref _reported402, PdfReaderDiagnosticCode.FontWidthsMalformed,
+ "has no /Widths and is not a standard 14 font.");
+ return false;
+ }
+
+ var firstCharResolved = Resolve(reader, fontDict.Get(_firstCharKey));
+ var lastCharResolved = Resolve(reader, fontDict.Get(_lastCharKey));
+ if (firstCharResolved is not PdfInteger first || first.Value is < 0 or > 255
+ || lastCharResolved is not PdfInteger last || last.Value is < 0 or > 255
+ || first.Value > last.Value)
+ {
+ ReportOnce(ref _reported402, PdfReaderDiagnosticCode.FontWidthsMalformed,
+ "/FirstChar or /LastChar is missing, mistyped, out of range, or FirstChar exceeds LastChar.");
+ return false;
+ }
+
+ if (widthsResolved is not PdfArray widthsArray)
+ {
+ ReportOnce(ref _reported402, PdfReaderDiagnosticCode.FontWidthsMalformed,
+ "/Widths is not an array.");
+ return false;
+ }
+
+ var firstChar = (int)first.Value;
+ var span = (int)(last.Value - first.Value + 1);
+ var usable = Math.Min(widthsArray.Count, span);
+ var malformed = widthsArray.Count < span;
+ for (var i = 0; i < usable; i++)
+ {
+ var element = Resolve(reader, widthsArray[i]);
+ switch (element)
+ {
+ case PdfInteger wi: widths[firstChar + i] = wi.Value; break;
+ case PdfReal wr: widths[firstChar + i] = wr.Value; break;
+ default: malformed = true; break;
+ }
+ }
+
+ if (malformed)
+ {
+ ReportOnce(ref _reported402, PdfReaderDiagnosticCode.FontWidthsMalformed,
+ "/Widths is shorter than LastChar - FirstChar + 1, or contains a non-number element.");
+ }
+ return false;
+ }
+
+ // Name-keyed for every one of the fourteen standard 14 fonts (SymbolFontMetrics' own
+ // generator reads all fourteen AFM files), so this lookup needs no Unicode round trip and no
+ // dependence on whether the glyph's Unicode value happens to fall inside WinAnsiEncoding: a
+ // text font's own AFM lists a width for every glyph name it defines, encodable in WinAnsi or
+ // not. The trade this keying makes: a /Differences name absent from the font's own AFM (a
+ // uniXXXX name, say, which no AFM's own N record ever uses) keeps MissingWidth rather than
+ // falling back through a Unicode round trip that might have found it. Glyph-name keying
+ // matches the AFM's own key, and a uniXXXX name in /Differences on a non-embedded standard 14
+ // font is a producer choice this AFM lookup was never going to be able to serve either way.
+ private static void FillAfmWidths(string afmName, string?[] table, double[] widths, double missingWidth)
+ {
+ var byName = SymbolFontMetrics.TryGetTextFontWidths(afmName, out var textWidths)
+ ? textWidths
+ : afmName == "Symbol" ? SymbolFontMetrics.SymbolWidths : SymbolFontMetrics.ZapfDingbatsWidths;
+ for (var code = 0; code < 256; code++)
+ {
+ var name = table[code];
+ if (name is null)
+ continue;
+ widths[code] = byName.TryGetValue(name, out var w) ? w : missingWidth;
+ }
+ }
+
+ ///
+ public override bool TryDecodeNext(ReadOnlySpan bytes, ref int offset, out DecodedGlyph glyph)
+ {
+ if (offset >= bytes.Length)
+ {
+ glyph = default;
+ return false;
+ }
+
+ var code = bytes[offset];
+ offset++;
+
+ // 404 is withheld while the font names a /ToUnicode stream this reader does not parse
+ // yet: that stream has priority over the glyph-name route (§9.10.2) and may map the code.
+ var unicode = _unicode[code];
+ if (unicode is null && !_reportedNoUnicodeOrUnmapped && _hasAnyMappedCode && !_hasToUnicode)
+ {
+ ReportOnce(ref _reportedNoUnicodeOrUnmapped, PdfReaderDiagnosticCode.UnmappedGlyphs,
+ "decoded a glyph whose code has no Unicode mapping, though other codes in this font do.");
+ }
+
+ glyph = new DecodedGlyph(code, 1, _widths[code], unicode, code == 32);
+ return true;
+ }
+
+ private void ReportOnce(ref bool flag, PdfReaderDiagnosticCode code, string message)
+ {
+ if (flag)
+ return;
+ flag = true;
+ _sink.Report(code, message, _objectNumber, _generation, _pageIndex);
+ }
+
+ ///
+ /// Null-tolerant single-hop resolution through . Also normalises a
+ /// resolved , direct or reached through the one hop, to
+ /// : ISO 32000-2 §7.3.7 states, verbatim, "A dictionary entry whose
+ /// value is null (see 7.3.9, "Null object") shall be treated the same as if the entry does
+ /// not exist", and every entry this class reads but /ToUnicode goes through this one
+ /// helper, so that rule applies uniformly to /Subtype, /Encoding,
+ /// /Widths, /FirstChar, /LastChar, /BaseFont,
+ /// /FontDescriptor, /MissingWidth, /BaseEncoding, /Flags and
+ /// /Differences from this one site. /ToUnicode takes
+ /// instead, which
+ /// returns null for both a dangling reference and a null target, so it reaches the same
+ /// outcome by its own route.
+ ///
+ private static PdfObject? Resolve(PdfDocumentReader reader, PdfObject? raw)
+ {
+ if (raw is null)
+ return null;
+ var resolved = reader.ResolveValue(raw);
+ return resolved is PdfNull ? null : resolved;
+ }
+}
diff --git a/src/VellumPdf.Reader/Fonts/Standard14Names.cs b/src/VellumPdf.Reader/Fonts/Standard14Names.cs
new file mode 100644
index 00000000..85c22346
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/Standard14Names.cs
@@ -0,0 +1,106 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// Resolves a font's /BaseFont name to one of the 14 standard fonts ISO 32000-2 §9.6.2.2
+/// names (Helvetica, Times, Courier in their four styles each, Symbol, ZapfDingbats), for the
+/// AFM-width fallback and the built-in encoding of a standard 14 font that omits /Widths
+/// and /FontDescriptor: Table 109 (§9.6.2.1) makes those entries optional for the
+/// standard 14 in PDF 1.0 to 1.7, and §9.6.2.2 requires a processor to have the fonts' metrics
+/// available.
+///
+///
+/// Beyond the 14 exact names, this class also recognises a fixed list of Windows/Word substitute
+/// names (Arial, Times New Roman, Courier New, and their bold/italic
+/// combinations) as aliases for the metrically closest standard font. ISO 32000-2 names only the
+/// 14 exact strings; this alias list is a reader heuristic with no basis in the standard, and it
+/// only ever selects a width table (the AFM fill in ), never a
+/// glyph mapping, which continues to come from the font's own /Encoding resolution
+/// regardless of which alias matched.
+///
+internal static class Standard14Names
+{
+ private static readonly Dictionary _aliases = new(StringComparer.Ordinal)
+ {
+ ["Arial"] = "Helvetica",
+ ["ArialMT"] = "Helvetica",
+ ["Arial,Bold"] = "Helvetica-Bold",
+ ["Arial-BoldMT"] = "Helvetica-Bold",
+ ["Arial,Italic"] = "Helvetica-Oblique",
+ ["Arial-ItalicMT"] = "Helvetica-Oblique",
+ ["Arial,BoldItalic"] = "Helvetica-BoldOblique",
+ ["Arial-BoldItalicMT"] = "Helvetica-BoldOblique",
+ ["Helvetica,Bold"] = "Helvetica-Bold",
+ ["Helvetica,Italic"] = "Helvetica-Oblique",
+ ["Helvetica,BoldItalic"] = "Helvetica-BoldOblique",
+ ["TimesNewRoman"] = "Times-Roman",
+ ["TimesNewRomanPSMT"] = "Times-Roman",
+ ["TimesNewRoman,Bold"] = "Times-Bold",
+ ["TimesNewRomanPS-BoldMT"] = "Times-Bold",
+ ["TimesNewRoman,Italic"] = "Times-Italic",
+ ["TimesNewRomanPS-ItalicMT"] = "Times-Italic",
+ ["TimesNewRoman,BoldItalic"] = "Times-BoldItalic",
+ ["TimesNewRomanPS-BoldItalicMT"] = "Times-BoldItalic",
+ ["CourierNew"] = "Courier",
+ ["CourierNewPSMT"] = "Courier",
+ ["CourierNew,Bold"] = "Courier-Bold",
+ ["CourierNewPS-BoldMT"] = "Courier-Bold",
+ ["CourierNew,Italic"] = "Courier-Oblique",
+ ["CourierNewPS-ItalicMT"] = "Courier-Oblique",
+ ["CourierNew,BoldItalic"] = "Courier-BoldOblique",
+ ["CourierNewPS-BoldItalicMT"] = "Courier-BoldOblique",
+ };
+
+ private static readonly HashSet _exact = new(StringComparer.Ordinal)
+ {
+ "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique",
+ "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic",
+ "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique",
+ "Symbol", "ZapfDingbats",
+ };
+
+ ///
+ /// Maps a /BaseFont name to the AFM font name it resolves to (e.g. Arial,Bold to
+ /// Helvetica-Bold, ABCDEF+Times-Roman to Times-Roman). Returns
+ /// when the name is longer than
+ /// , or is neither one of the 14 exact names nor
+ /// a documented alias. Comparison is case-sensitive, matching the standard's own names.
+ ///
+ public static bool TryResolve(string baseFont, out string afmName)
+ {
+ afmName = "";
+ if (baseFont.Length == 0 || baseFont.Length > AdobeGlyphList.MaxGlyphNameLength)
+ return false;
+
+ // A subset tag is exactly six uppercase letters followed by '+' (ISO 32000-2 §9.9.2).
+ var name = baseFont;
+ if (name.Length > 7 && name[6] == '+' && IsSubsetTag(name))
+ name = name[7..];
+
+ if (_exact.Contains(name))
+ {
+ afmName = name;
+ return true;
+ }
+
+ if (_aliases.TryGetValue(name, out var resolved))
+ {
+ afmName = resolved;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsSubsetTag(string name)
+ {
+ for (var i = 0; i < 6; i++)
+ {
+ if (name[i] is < 'A' or > 'Z')
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs
new file mode 100644
index 00000000..55048cb0
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs
@@ -0,0 +1,4793 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Collections.Frozen;
+
+// Generated by eng/generate-symbol-font-metrics.py; do not edit by hand.
+//
+// Inputs (Adobe Core-14 AFM files, MustRead.html, Adobe Systems, 1997), each one's own
+// Version line, and the normalised SHA-256 this generator pinned it against:
+// Symbol.afm (Version 001.008)
+// a336805b37aa468ba403bcae995652e9b335994462ab3703a572ed5bb87363d7
+// ZapfDingbats.afm (Version 002.000)
+// b56fbcaebd71b210ba4cfac4bb669764c4f1bd7ab523f37f01b2f04f079bf699
+// Helvetica.afm (Version 002.000)
+// 79e23df3e75df921fb8666fceb35b7ca363737dc9b5519cd4c1d9d1d2c23599c
+// Helvetica-Bold.afm (Version 002.000)
+// 5f455fcdbf5583c3b9bc5d884d352eef3aed330396f0467328fac89eb6f4c740
+// Helvetica-Oblique.afm (Version 002.000)
+// 91ed92f7b46b73fe062ad22667c2c462eafc0197288d9d70b989209b83e767f4
+// Helvetica-BoldOblique.afm (Version 002.000)
+// 910c39255d66d14b08e64ced7ecca0511f974940ca5da93c82bb41e61519d9f2
+// Times-Roman.afm (Version 002.000)
+// 41928f144173929c15f8cd4cc19aafeead3bd82b22141e55c538e94af7449ecf
+// Times-Bold.afm (Version 002.000)
+// c29b638a607a3347fa42a57adbfe98779681fa99b8b7c0ef903ff6ec6075d01c
+// Times-Italic.afm (Version 002.000)
+// 74d5c65a553790bf3844cc515b591fcf9b2cc7b9b8472e114bdc231d5bb551af
+// Times-BoldItalic.afm (Version 002.000)
+// ea8b71d0a1f8cdb6835bec1f37dcef6ab1eeee1bca3c6f88b07a85eef4d1455e
+// Courier.afm (Version 003.000)
+// bdeeadc7738eccd3deb220e26d65152e6cbafaffc70d9f6c73a5461a893f16eb
+// Courier-Bold.afm (Version 003.000)
+// 132f204bdeb88061b6180d7e8a9dd1a6fd313c5e8da5a64af0c0464b8155d879
+// Courier-Oblique.afm (Version 003.000)
+// 728a2c018b50f40368dbdaa393bb97937e713c2e0f4841e40e4abafe02fdb09b
+// Courier-BoldOblique.afm (Version 003.000)
+// 780a896a2a331066e8ed39024833ba6864fb4286f775d285186fae5aa348c436
+//
+// Symbol.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.
+// All rights reserved.
+// ZapfDingbats.afm: Comment Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// ZapfDingbats.afm: ITC Zapf Dingbats is a registered trademark of International Typeface
+// Corporation.
+// Helvetica.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.
+// All Rights Reserved.
+// Helvetica.afm: Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Helvetica-Bold.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Helvetica-Bold.afm: Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Helvetica-Oblique.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Helvetica-Oblique.afm: Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Helvetica-BoldOblique.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Helvetica-BoldOblique.afm: Helvetica is a trademark of Linotype-Hell AG and/or its
+// subsidiaries.
+// Times-Roman.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Times-Roman.afm: Times is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Times-Bold.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Times-Bold.afm: Times is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Times-Italic.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Times-Italic.afm: Times is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Times-BoldItalic.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Times-BoldItalic.afm: Times is a trademark of Linotype-Hell AG and/or its subsidiaries.
+// Courier.afm: Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Courier-Bold.afm: Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Courier-Oblique.afm: Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+// Courier-BoldOblique.afm: Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+//
+// This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and
+// distributed for any purpose and without charge, with or without modification, provided that
+// all copyright notices are retained; that the AFM files are not distributed without this file;
+// that all modifications to this file or any of the AFM files are prominently noted in the
+// modified file(s); and that this paragraph is not modified. Adobe Systems has no
+// responsibility or obligation to support the use of the AFM files.
+//
+// This file is a derived table of glyph names, codes and advance widths, not a copy of
+// the AFM files.
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// The built-in encodings of the two symbolic standard 14 fonts (Symbol and ZapfDingbats)
+/// and the glyph-name-keyed advance widths of all fourteen. ISO 32000-2 Annex D.1 names
+/// Annex D.5 and D.6 as the symbolic pair's built-in encodings; the Adobe Core-14 AFM
+/// files are this reader's delivery vehicle for that same data, not a separate
+/// transcription of the Annex D tables. The Symbol coding here agrees with Annex D.5 at
+/// all 189 coded glyphs. The ZapfDingbats coding carries 14 codes (0x80 to 0x8D) that
+/// Annex D.6 does not document at all; this reader keeps them, on the view that a font
+/// program carrying those codes draws them regardless of whether the standard's own
+/// table lists them. The twelve nonsymbolic text fonts carry no built-in encoding here
+/// (their glyph names come from and /Differences
+/// instead); only their AFM widths are kept, keyed by glyph name so a name outside
+/// WinAnsiEncoding still measures at its own width rather than a substitute glyph's.
+///
+internal static class SymbolFontMetrics
+{
+ /// Symbol's built-in encoding (ISO 32000-2 Annex D.5): char code to glyph
+ /// name, null where the AFM assigns the code no glyph.
+ public static ReadOnlySpan SymbolEncoding => _symbol;
+
+ /// ZapfDingbats' built-in encoding (ISO 32000-2 Annex D.6, plus the 14 codes
+ /// the class doc above names): char code to glyph name.
+ public static ReadOnlySpan ZapfDingbatsEncoding => _zapfDingbats;
+
+ /// Symbol's AFM advance widths, name-keyed (includes "apple", which the
+ /// AFM assigns no code (C -1), so it is absent from
+ /// ).
+ public static IReadOnlyDictionary SymbolWidths => _symbolWidths;
+
+ /// ZapfDingbats' AFM advance widths, name-keyed.
+ public static IReadOnlyDictionary ZapfDingbatsWidths => _zapfDingbatsWidths;
+
+ ///
+ /// The named text font's AFM advance widths, keyed by glyph name (the AFM's own N
+ /// records, including names the font's own StandardEncoding table has no code for).
+ /// is one of the twelve exact names
+ /// Standard14Names.TryResolve can produce (e.g. "Helvetica-Bold").
+ /// Returns for "Symbol", "ZapfDingbats", or
+ /// any other name.
+ ///
+ public static bool TryGetTextFontWidths(string afmName, out IReadOnlyDictionary widths)
+ {
+ if (_textFontWidths.TryGetValue(afmName, out var found))
+ {
+ widths = found;
+ return true;
+ }
+ widths = FrozenDictionary.Empty;
+ return false;
+ }
+
+ private static readonly string?[] _symbol = BuildEncoding_symbol();
+
+ private static readonly string?[] _zapfDingbats = BuildEncoding_zapfDingbats();
+
+ private static readonly FrozenDictionary _helveticaWidths = new Dictionary
+ {
+ ["space"] = 278,
+ ["exclam"] = 278,
+ ["quotedbl"] = 355,
+ ["numbersign"] = 556,
+ ["dollar"] = 556,
+ ["percent"] = 889,
+ ["ampersand"] = 667,
+ ["quoteright"] = 222,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 389,
+ ["plus"] = 584,
+ ["comma"] = 278,
+ ["hyphen"] = 333,
+ ["period"] = 278,
+ ["slash"] = 278,
+ ["zero"] = 556,
+ ["one"] = 556,
+ ["two"] = 556,
+ ["three"] = 556,
+ ["four"] = 556,
+ ["five"] = 556,
+ ["six"] = 556,
+ ["seven"] = 556,
+ ["eight"] = 556,
+ ["nine"] = 556,
+ ["colon"] = 278,
+ ["semicolon"] = 278,
+ ["less"] = 584,
+ ["equal"] = 584,
+ ["greater"] = 584,
+ ["question"] = 556,
+ ["at"] = 1015,
+ ["A"] = 667,
+ ["B"] = 667,
+ ["C"] = 722,
+ ["D"] = 722,
+ ["E"] = 667,
+ ["F"] = 611,
+ ["G"] = 778,
+ ["H"] = 722,
+ ["I"] = 278,
+ ["J"] = 500,
+ ["K"] = 667,
+ ["L"] = 556,
+ ["M"] = 833,
+ ["N"] = 722,
+ ["O"] = 778,
+ ["P"] = 667,
+ ["Q"] = 778,
+ ["R"] = 722,
+ ["S"] = 667,
+ ["T"] = 611,
+ ["U"] = 722,
+ ["V"] = 667,
+ ["W"] = 944,
+ ["X"] = 667,
+ ["Y"] = 667,
+ ["Z"] = 611,
+ ["bracketleft"] = 278,
+ ["backslash"] = 278,
+ ["bracketright"] = 278,
+ ["asciicircum"] = 469,
+ ["underscore"] = 556,
+ ["quoteleft"] = 222,
+ ["a"] = 556,
+ ["b"] = 556,
+ ["c"] = 500,
+ ["d"] = 556,
+ ["e"] = 556,
+ ["f"] = 278,
+ ["g"] = 556,
+ ["h"] = 556,
+ ["i"] = 222,
+ ["j"] = 222,
+ ["k"] = 500,
+ ["l"] = 222,
+ ["m"] = 833,
+ ["n"] = 556,
+ ["o"] = 556,
+ ["p"] = 556,
+ ["q"] = 556,
+ ["r"] = 333,
+ ["s"] = 500,
+ ["t"] = 278,
+ ["u"] = 556,
+ ["v"] = 500,
+ ["w"] = 722,
+ ["x"] = 500,
+ ["y"] = 500,
+ ["z"] = 500,
+ ["braceleft"] = 334,
+ ["bar"] = 260,
+ ["braceright"] = 334,
+ ["asciitilde"] = 584,
+ ["exclamdown"] = 333,
+ ["cent"] = 556,
+ ["sterling"] = 556,
+ ["fraction"] = 167,
+ ["yen"] = 556,
+ ["florin"] = 556,
+ ["section"] = 556,
+ ["currency"] = 556,
+ ["quotesingle"] = 191,
+ ["quotedblleft"] = 333,
+ ["guillemotleft"] = 556,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 500,
+ ["fl"] = 500,
+ ["endash"] = 556,
+ ["dagger"] = 556,
+ ["daggerdbl"] = 556,
+ ["periodcentered"] = 278,
+ ["paragraph"] = 537,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 222,
+ ["quotedblbase"] = 333,
+ ["quotedblright"] = 333,
+ ["guillemotright"] = 556,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 611,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 1000,
+ ["ordfeminine"] = 370,
+ ["Lslash"] = 556,
+ ["Oslash"] = 778,
+ ["OE"] = 1000,
+ ["ordmasculine"] = 365,
+ ["ae"] = 889,
+ ["dotlessi"] = 278,
+ ["lslash"] = 222,
+ ["oslash"] = 611,
+ ["oe"] = 944,
+ ["germandbls"] = 611,
+ ["Idieresis"] = 278,
+ ["eacute"] = 556,
+ ["abreve"] = 556,
+ ["uhungarumlaut"] = 556,
+ ["ecaron"] = 556,
+ ["Ydieresis"] = 667,
+ ["divide"] = 584,
+ ["Yacute"] = 667,
+ ["Acircumflex"] = 667,
+ ["aacute"] = 556,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 500,
+ ["scommaaccent"] = 500,
+ ["ecircumflex"] = 556,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 556,
+ ["Uacute"] = 722,
+ ["uogonek"] = 556,
+ ["Edieresis"] = 667,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 737,
+ ["Emacron"] = 667,
+ ["ccaron"] = 500,
+ ["aring"] = 556,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 222,
+ ["agrave"] = 556,
+ ["Tcommaaccent"] = 611,
+ ["Cacute"] = 722,
+ ["atilde"] = 556,
+ ["Edotaccent"] = 667,
+ ["scaron"] = 500,
+ ["scedilla"] = 500,
+ ["iacute"] = 278,
+ ["lozenge"] = 471,
+ ["Rcaron"] = 722,
+ ["Gcommaaccent"] = 778,
+ ["ucircumflex"] = 556,
+ ["acircumflex"] = 556,
+ ["Amacron"] = 667,
+ ["rcaron"] = 333,
+ ["ccedilla"] = 500,
+ ["Zdotaccent"] = 611,
+ ["Thorn"] = 667,
+ ["Omacron"] = 778,
+ ["Racute"] = 722,
+ ["Sacute"] = 667,
+ ["dcaron"] = 643,
+ ["Umacron"] = 722,
+ ["uring"] = 556,
+ ["threesuperior"] = 333,
+ ["Ograve"] = 778,
+ ["Agrave"] = 667,
+ ["Abreve"] = 667,
+ ["multiply"] = 584,
+ ["uacute"] = 556,
+ ["Tcaron"] = 611,
+ ["partialdiff"] = 476,
+ ["ydieresis"] = 500,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 667,
+ ["adieresis"] = 556,
+ ["edieresis"] = 556,
+ ["cacute"] = 500,
+ ["nacute"] = 556,
+ ["umacron"] = 556,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 278,
+ ["plusminus"] = 584,
+ ["brokenbar"] = 260,
+ ["registered"] = 737,
+ ["Gbreve"] = 778,
+ ["Idotaccent"] = 278,
+ ["summation"] = 600,
+ ["Egrave"] = 667,
+ ["racute"] = 333,
+ ["omacron"] = 556,
+ ["Zacute"] = 611,
+ ["Zcaron"] = 611,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 722,
+ ["lcommaaccent"] = 222,
+ ["tcaron"] = 317,
+ ["eogonek"] = 556,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 667,
+ ["Adieresis"] = 667,
+ ["egrave"] = 556,
+ ["zacute"] = 500,
+ ["iogonek"] = 222,
+ ["Oacute"] = 778,
+ ["oacute"] = 556,
+ ["amacron"] = 556,
+ ["sacute"] = 500,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 778,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 556,
+ ["twosuperior"] = 333,
+ ["Odieresis"] = 778,
+ ["mu"] = 556,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 556,
+ ["Eogonek"] = 667,
+ ["dcroat"] = 556,
+ ["threequarters"] = 834,
+ ["Scedilla"] = 667,
+ ["lcaron"] = 299,
+ ["Kcommaaccent"] = 667,
+ ["Lacute"] = 556,
+ ["trademark"] = 1000,
+ ["edotaccent"] = 556,
+ ["Igrave"] = 278,
+ ["Imacron"] = 278,
+ ["Lcaron"] = 556,
+ ["onehalf"] = 834,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 556,
+ ["ntilde"] = 556,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 667,
+ ["emacron"] = 556,
+ ["gbreve"] = 556,
+ ["onequarter"] = 834,
+ ["Scaron"] = 667,
+ ["Scommaaccent"] = 667,
+ ["Ohungarumlaut"] = 778,
+ ["degree"] = 400,
+ ["ograve"] = 556,
+ ["Ccaron"] = 722,
+ ["ugrave"] = 556,
+ ["radical"] = 453,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 333,
+ ["Ntilde"] = 722,
+ ["otilde"] = 556,
+ ["Rcommaaccent"] = 722,
+ ["Lcommaaccent"] = 556,
+ ["Atilde"] = 667,
+ ["Aogonek"] = 667,
+ ["Aring"] = 667,
+ ["Otilde"] = 778,
+ ["zdotaccent"] = 500,
+ ["Ecaron"] = 667,
+ ["Iogonek"] = 278,
+ ["kcommaaccent"] = 500,
+ ["minus"] = 584,
+ ["Icircumflex"] = 278,
+ ["ncaron"] = 556,
+ ["tcommaaccent"] = 278,
+ ["logicalnot"] = 584,
+ ["odieresis"] = 556,
+ ["udieresis"] = 556,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 556,
+ ["eth"] = 556,
+ ["zcaron"] = 500,
+ ["ncommaaccent"] = 556,
+ ["onesuperior"] = 333,
+ ["imacron"] = 278,
+ ["Euro"] = 556,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _helveticaBoldWidths = new Dictionary
+ {
+ ["space"] = 278,
+ ["exclam"] = 333,
+ ["quotedbl"] = 474,
+ ["numbersign"] = 556,
+ ["dollar"] = 556,
+ ["percent"] = 889,
+ ["ampersand"] = 722,
+ ["quoteright"] = 278,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 389,
+ ["plus"] = 584,
+ ["comma"] = 278,
+ ["hyphen"] = 333,
+ ["period"] = 278,
+ ["slash"] = 278,
+ ["zero"] = 556,
+ ["one"] = 556,
+ ["two"] = 556,
+ ["three"] = 556,
+ ["four"] = 556,
+ ["five"] = 556,
+ ["six"] = 556,
+ ["seven"] = 556,
+ ["eight"] = 556,
+ ["nine"] = 556,
+ ["colon"] = 333,
+ ["semicolon"] = 333,
+ ["less"] = 584,
+ ["equal"] = 584,
+ ["greater"] = 584,
+ ["question"] = 611,
+ ["at"] = 975,
+ ["A"] = 722,
+ ["B"] = 722,
+ ["C"] = 722,
+ ["D"] = 722,
+ ["E"] = 667,
+ ["F"] = 611,
+ ["G"] = 778,
+ ["H"] = 722,
+ ["I"] = 278,
+ ["J"] = 556,
+ ["K"] = 722,
+ ["L"] = 611,
+ ["M"] = 833,
+ ["N"] = 722,
+ ["O"] = 778,
+ ["P"] = 667,
+ ["Q"] = 778,
+ ["R"] = 722,
+ ["S"] = 667,
+ ["T"] = 611,
+ ["U"] = 722,
+ ["V"] = 667,
+ ["W"] = 944,
+ ["X"] = 667,
+ ["Y"] = 667,
+ ["Z"] = 611,
+ ["bracketleft"] = 333,
+ ["backslash"] = 278,
+ ["bracketright"] = 333,
+ ["asciicircum"] = 584,
+ ["underscore"] = 556,
+ ["quoteleft"] = 278,
+ ["a"] = 556,
+ ["b"] = 611,
+ ["c"] = 556,
+ ["d"] = 611,
+ ["e"] = 556,
+ ["f"] = 333,
+ ["g"] = 611,
+ ["h"] = 611,
+ ["i"] = 278,
+ ["j"] = 278,
+ ["k"] = 556,
+ ["l"] = 278,
+ ["m"] = 889,
+ ["n"] = 611,
+ ["o"] = 611,
+ ["p"] = 611,
+ ["q"] = 611,
+ ["r"] = 389,
+ ["s"] = 556,
+ ["t"] = 333,
+ ["u"] = 611,
+ ["v"] = 556,
+ ["w"] = 778,
+ ["x"] = 556,
+ ["y"] = 556,
+ ["z"] = 500,
+ ["braceleft"] = 389,
+ ["bar"] = 280,
+ ["braceright"] = 389,
+ ["asciitilde"] = 584,
+ ["exclamdown"] = 333,
+ ["cent"] = 556,
+ ["sterling"] = 556,
+ ["fraction"] = 167,
+ ["yen"] = 556,
+ ["florin"] = 556,
+ ["section"] = 556,
+ ["currency"] = 556,
+ ["quotesingle"] = 238,
+ ["quotedblleft"] = 500,
+ ["guillemotleft"] = 556,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 611,
+ ["fl"] = 611,
+ ["endash"] = 556,
+ ["dagger"] = 556,
+ ["daggerdbl"] = 556,
+ ["periodcentered"] = 278,
+ ["paragraph"] = 556,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 278,
+ ["quotedblbase"] = 500,
+ ["quotedblright"] = 500,
+ ["guillemotright"] = 556,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 611,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 1000,
+ ["ordfeminine"] = 370,
+ ["Lslash"] = 611,
+ ["Oslash"] = 778,
+ ["OE"] = 1000,
+ ["ordmasculine"] = 365,
+ ["ae"] = 889,
+ ["dotlessi"] = 278,
+ ["lslash"] = 278,
+ ["oslash"] = 611,
+ ["oe"] = 944,
+ ["germandbls"] = 611,
+ ["Idieresis"] = 278,
+ ["eacute"] = 556,
+ ["abreve"] = 556,
+ ["uhungarumlaut"] = 611,
+ ["ecaron"] = 556,
+ ["Ydieresis"] = 667,
+ ["divide"] = 584,
+ ["Yacute"] = 667,
+ ["Acircumflex"] = 722,
+ ["aacute"] = 556,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 556,
+ ["scommaaccent"] = 556,
+ ["ecircumflex"] = 556,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 556,
+ ["Uacute"] = 722,
+ ["uogonek"] = 611,
+ ["Edieresis"] = 667,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 737,
+ ["Emacron"] = 667,
+ ["ccaron"] = 556,
+ ["aring"] = 556,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 278,
+ ["agrave"] = 556,
+ ["Tcommaaccent"] = 611,
+ ["Cacute"] = 722,
+ ["atilde"] = 556,
+ ["Edotaccent"] = 667,
+ ["scaron"] = 556,
+ ["scedilla"] = 556,
+ ["iacute"] = 278,
+ ["lozenge"] = 494,
+ ["Rcaron"] = 722,
+ ["Gcommaaccent"] = 778,
+ ["ucircumflex"] = 611,
+ ["acircumflex"] = 556,
+ ["Amacron"] = 722,
+ ["rcaron"] = 389,
+ ["ccedilla"] = 556,
+ ["Zdotaccent"] = 611,
+ ["Thorn"] = 667,
+ ["Omacron"] = 778,
+ ["Racute"] = 722,
+ ["Sacute"] = 667,
+ ["dcaron"] = 743,
+ ["Umacron"] = 722,
+ ["uring"] = 611,
+ ["threesuperior"] = 333,
+ ["Ograve"] = 778,
+ ["Agrave"] = 722,
+ ["Abreve"] = 722,
+ ["multiply"] = 584,
+ ["uacute"] = 611,
+ ["Tcaron"] = 611,
+ ["partialdiff"] = 494,
+ ["ydieresis"] = 556,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 667,
+ ["adieresis"] = 556,
+ ["edieresis"] = 556,
+ ["cacute"] = 556,
+ ["nacute"] = 611,
+ ["umacron"] = 611,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 278,
+ ["plusminus"] = 584,
+ ["brokenbar"] = 280,
+ ["registered"] = 737,
+ ["Gbreve"] = 778,
+ ["Idotaccent"] = 278,
+ ["summation"] = 600,
+ ["Egrave"] = 667,
+ ["racute"] = 389,
+ ["omacron"] = 611,
+ ["Zacute"] = 611,
+ ["Zcaron"] = 611,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 722,
+ ["lcommaaccent"] = 278,
+ ["tcaron"] = 389,
+ ["eogonek"] = 556,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 722,
+ ["Adieresis"] = 722,
+ ["egrave"] = 556,
+ ["zacute"] = 500,
+ ["iogonek"] = 278,
+ ["Oacute"] = 778,
+ ["oacute"] = 611,
+ ["amacron"] = 556,
+ ["sacute"] = 556,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 778,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 611,
+ ["twosuperior"] = 333,
+ ["Odieresis"] = 778,
+ ["mu"] = 611,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 611,
+ ["Eogonek"] = 667,
+ ["dcroat"] = 611,
+ ["threequarters"] = 834,
+ ["Scedilla"] = 667,
+ ["lcaron"] = 400,
+ ["Kcommaaccent"] = 722,
+ ["Lacute"] = 611,
+ ["trademark"] = 1000,
+ ["edotaccent"] = 556,
+ ["Igrave"] = 278,
+ ["Imacron"] = 278,
+ ["Lcaron"] = 611,
+ ["onehalf"] = 834,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 611,
+ ["ntilde"] = 611,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 667,
+ ["emacron"] = 556,
+ ["gbreve"] = 611,
+ ["onequarter"] = 834,
+ ["Scaron"] = 667,
+ ["Scommaaccent"] = 667,
+ ["Ohungarumlaut"] = 778,
+ ["degree"] = 400,
+ ["ograve"] = 611,
+ ["Ccaron"] = 722,
+ ["ugrave"] = 611,
+ ["radical"] = 549,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 389,
+ ["Ntilde"] = 722,
+ ["otilde"] = 611,
+ ["Rcommaaccent"] = 722,
+ ["Lcommaaccent"] = 611,
+ ["Atilde"] = 722,
+ ["Aogonek"] = 722,
+ ["Aring"] = 722,
+ ["Otilde"] = 778,
+ ["zdotaccent"] = 500,
+ ["Ecaron"] = 667,
+ ["Iogonek"] = 278,
+ ["kcommaaccent"] = 556,
+ ["minus"] = 584,
+ ["Icircumflex"] = 278,
+ ["ncaron"] = 611,
+ ["tcommaaccent"] = 333,
+ ["logicalnot"] = 584,
+ ["odieresis"] = 611,
+ ["udieresis"] = 611,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 611,
+ ["eth"] = 611,
+ ["zcaron"] = 500,
+ ["ncommaaccent"] = 611,
+ ["onesuperior"] = 333,
+ ["imacron"] = 278,
+ ["Euro"] = 556,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _helveticaObliqueWidths = new Dictionary
+ {
+ ["space"] = 278,
+ ["exclam"] = 278,
+ ["quotedbl"] = 355,
+ ["numbersign"] = 556,
+ ["dollar"] = 556,
+ ["percent"] = 889,
+ ["ampersand"] = 667,
+ ["quoteright"] = 222,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 389,
+ ["plus"] = 584,
+ ["comma"] = 278,
+ ["hyphen"] = 333,
+ ["period"] = 278,
+ ["slash"] = 278,
+ ["zero"] = 556,
+ ["one"] = 556,
+ ["two"] = 556,
+ ["three"] = 556,
+ ["four"] = 556,
+ ["five"] = 556,
+ ["six"] = 556,
+ ["seven"] = 556,
+ ["eight"] = 556,
+ ["nine"] = 556,
+ ["colon"] = 278,
+ ["semicolon"] = 278,
+ ["less"] = 584,
+ ["equal"] = 584,
+ ["greater"] = 584,
+ ["question"] = 556,
+ ["at"] = 1015,
+ ["A"] = 667,
+ ["B"] = 667,
+ ["C"] = 722,
+ ["D"] = 722,
+ ["E"] = 667,
+ ["F"] = 611,
+ ["G"] = 778,
+ ["H"] = 722,
+ ["I"] = 278,
+ ["J"] = 500,
+ ["K"] = 667,
+ ["L"] = 556,
+ ["M"] = 833,
+ ["N"] = 722,
+ ["O"] = 778,
+ ["P"] = 667,
+ ["Q"] = 778,
+ ["R"] = 722,
+ ["S"] = 667,
+ ["T"] = 611,
+ ["U"] = 722,
+ ["V"] = 667,
+ ["W"] = 944,
+ ["X"] = 667,
+ ["Y"] = 667,
+ ["Z"] = 611,
+ ["bracketleft"] = 278,
+ ["backslash"] = 278,
+ ["bracketright"] = 278,
+ ["asciicircum"] = 469,
+ ["underscore"] = 556,
+ ["quoteleft"] = 222,
+ ["a"] = 556,
+ ["b"] = 556,
+ ["c"] = 500,
+ ["d"] = 556,
+ ["e"] = 556,
+ ["f"] = 278,
+ ["g"] = 556,
+ ["h"] = 556,
+ ["i"] = 222,
+ ["j"] = 222,
+ ["k"] = 500,
+ ["l"] = 222,
+ ["m"] = 833,
+ ["n"] = 556,
+ ["o"] = 556,
+ ["p"] = 556,
+ ["q"] = 556,
+ ["r"] = 333,
+ ["s"] = 500,
+ ["t"] = 278,
+ ["u"] = 556,
+ ["v"] = 500,
+ ["w"] = 722,
+ ["x"] = 500,
+ ["y"] = 500,
+ ["z"] = 500,
+ ["braceleft"] = 334,
+ ["bar"] = 260,
+ ["braceright"] = 334,
+ ["asciitilde"] = 584,
+ ["exclamdown"] = 333,
+ ["cent"] = 556,
+ ["sterling"] = 556,
+ ["fraction"] = 167,
+ ["yen"] = 556,
+ ["florin"] = 556,
+ ["section"] = 556,
+ ["currency"] = 556,
+ ["quotesingle"] = 191,
+ ["quotedblleft"] = 333,
+ ["guillemotleft"] = 556,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 500,
+ ["fl"] = 500,
+ ["endash"] = 556,
+ ["dagger"] = 556,
+ ["daggerdbl"] = 556,
+ ["periodcentered"] = 278,
+ ["paragraph"] = 537,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 222,
+ ["quotedblbase"] = 333,
+ ["quotedblright"] = 333,
+ ["guillemotright"] = 556,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 611,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 1000,
+ ["ordfeminine"] = 370,
+ ["Lslash"] = 556,
+ ["Oslash"] = 778,
+ ["OE"] = 1000,
+ ["ordmasculine"] = 365,
+ ["ae"] = 889,
+ ["dotlessi"] = 278,
+ ["lslash"] = 222,
+ ["oslash"] = 611,
+ ["oe"] = 944,
+ ["germandbls"] = 611,
+ ["Idieresis"] = 278,
+ ["eacute"] = 556,
+ ["abreve"] = 556,
+ ["uhungarumlaut"] = 556,
+ ["ecaron"] = 556,
+ ["Ydieresis"] = 667,
+ ["divide"] = 584,
+ ["Yacute"] = 667,
+ ["Acircumflex"] = 667,
+ ["aacute"] = 556,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 500,
+ ["scommaaccent"] = 500,
+ ["ecircumflex"] = 556,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 556,
+ ["Uacute"] = 722,
+ ["uogonek"] = 556,
+ ["Edieresis"] = 667,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 737,
+ ["Emacron"] = 667,
+ ["ccaron"] = 500,
+ ["aring"] = 556,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 222,
+ ["agrave"] = 556,
+ ["Tcommaaccent"] = 611,
+ ["Cacute"] = 722,
+ ["atilde"] = 556,
+ ["Edotaccent"] = 667,
+ ["scaron"] = 500,
+ ["scedilla"] = 500,
+ ["iacute"] = 278,
+ ["lozenge"] = 471,
+ ["Rcaron"] = 722,
+ ["Gcommaaccent"] = 778,
+ ["ucircumflex"] = 556,
+ ["acircumflex"] = 556,
+ ["Amacron"] = 667,
+ ["rcaron"] = 333,
+ ["ccedilla"] = 500,
+ ["Zdotaccent"] = 611,
+ ["Thorn"] = 667,
+ ["Omacron"] = 778,
+ ["Racute"] = 722,
+ ["Sacute"] = 667,
+ ["dcaron"] = 643,
+ ["Umacron"] = 722,
+ ["uring"] = 556,
+ ["threesuperior"] = 333,
+ ["Ograve"] = 778,
+ ["Agrave"] = 667,
+ ["Abreve"] = 667,
+ ["multiply"] = 584,
+ ["uacute"] = 556,
+ ["Tcaron"] = 611,
+ ["partialdiff"] = 476,
+ ["ydieresis"] = 500,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 667,
+ ["adieresis"] = 556,
+ ["edieresis"] = 556,
+ ["cacute"] = 500,
+ ["nacute"] = 556,
+ ["umacron"] = 556,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 278,
+ ["plusminus"] = 584,
+ ["brokenbar"] = 260,
+ ["registered"] = 737,
+ ["Gbreve"] = 778,
+ ["Idotaccent"] = 278,
+ ["summation"] = 600,
+ ["Egrave"] = 667,
+ ["racute"] = 333,
+ ["omacron"] = 556,
+ ["Zacute"] = 611,
+ ["Zcaron"] = 611,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 722,
+ ["lcommaaccent"] = 222,
+ ["tcaron"] = 317,
+ ["eogonek"] = 556,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 667,
+ ["Adieresis"] = 667,
+ ["egrave"] = 556,
+ ["zacute"] = 500,
+ ["iogonek"] = 222,
+ ["Oacute"] = 778,
+ ["oacute"] = 556,
+ ["amacron"] = 556,
+ ["sacute"] = 500,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 778,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 556,
+ ["twosuperior"] = 333,
+ ["Odieresis"] = 778,
+ ["mu"] = 556,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 556,
+ ["Eogonek"] = 667,
+ ["dcroat"] = 556,
+ ["threequarters"] = 834,
+ ["Scedilla"] = 667,
+ ["lcaron"] = 299,
+ ["Kcommaaccent"] = 667,
+ ["Lacute"] = 556,
+ ["trademark"] = 1000,
+ ["edotaccent"] = 556,
+ ["Igrave"] = 278,
+ ["Imacron"] = 278,
+ ["Lcaron"] = 556,
+ ["onehalf"] = 834,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 556,
+ ["ntilde"] = 556,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 667,
+ ["emacron"] = 556,
+ ["gbreve"] = 556,
+ ["onequarter"] = 834,
+ ["Scaron"] = 667,
+ ["Scommaaccent"] = 667,
+ ["Ohungarumlaut"] = 778,
+ ["degree"] = 400,
+ ["ograve"] = 556,
+ ["Ccaron"] = 722,
+ ["ugrave"] = 556,
+ ["radical"] = 453,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 333,
+ ["Ntilde"] = 722,
+ ["otilde"] = 556,
+ ["Rcommaaccent"] = 722,
+ ["Lcommaaccent"] = 556,
+ ["Atilde"] = 667,
+ ["Aogonek"] = 667,
+ ["Aring"] = 667,
+ ["Otilde"] = 778,
+ ["zdotaccent"] = 500,
+ ["Ecaron"] = 667,
+ ["Iogonek"] = 278,
+ ["kcommaaccent"] = 500,
+ ["minus"] = 584,
+ ["Icircumflex"] = 278,
+ ["ncaron"] = 556,
+ ["tcommaaccent"] = 278,
+ ["logicalnot"] = 584,
+ ["odieresis"] = 556,
+ ["udieresis"] = 556,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 556,
+ ["eth"] = 556,
+ ["zcaron"] = 500,
+ ["ncommaaccent"] = 556,
+ ["onesuperior"] = 333,
+ ["imacron"] = 278,
+ ["Euro"] = 556,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _helveticaBoldObliqueWidths = new Dictionary
+ {
+ ["space"] = 278,
+ ["exclam"] = 333,
+ ["quotedbl"] = 474,
+ ["numbersign"] = 556,
+ ["dollar"] = 556,
+ ["percent"] = 889,
+ ["ampersand"] = 722,
+ ["quoteright"] = 278,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 389,
+ ["plus"] = 584,
+ ["comma"] = 278,
+ ["hyphen"] = 333,
+ ["period"] = 278,
+ ["slash"] = 278,
+ ["zero"] = 556,
+ ["one"] = 556,
+ ["two"] = 556,
+ ["three"] = 556,
+ ["four"] = 556,
+ ["five"] = 556,
+ ["six"] = 556,
+ ["seven"] = 556,
+ ["eight"] = 556,
+ ["nine"] = 556,
+ ["colon"] = 333,
+ ["semicolon"] = 333,
+ ["less"] = 584,
+ ["equal"] = 584,
+ ["greater"] = 584,
+ ["question"] = 611,
+ ["at"] = 975,
+ ["A"] = 722,
+ ["B"] = 722,
+ ["C"] = 722,
+ ["D"] = 722,
+ ["E"] = 667,
+ ["F"] = 611,
+ ["G"] = 778,
+ ["H"] = 722,
+ ["I"] = 278,
+ ["J"] = 556,
+ ["K"] = 722,
+ ["L"] = 611,
+ ["M"] = 833,
+ ["N"] = 722,
+ ["O"] = 778,
+ ["P"] = 667,
+ ["Q"] = 778,
+ ["R"] = 722,
+ ["S"] = 667,
+ ["T"] = 611,
+ ["U"] = 722,
+ ["V"] = 667,
+ ["W"] = 944,
+ ["X"] = 667,
+ ["Y"] = 667,
+ ["Z"] = 611,
+ ["bracketleft"] = 333,
+ ["backslash"] = 278,
+ ["bracketright"] = 333,
+ ["asciicircum"] = 584,
+ ["underscore"] = 556,
+ ["quoteleft"] = 278,
+ ["a"] = 556,
+ ["b"] = 611,
+ ["c"] = 556,
+ ["d"] = 611,
+ ["e"] = 556,
+ ["f"] = 333,
+ ["g"] = 611,
+ ["h"] = 611,
+ ["i"] = 278,
+ ["j"] = 278,
+ ["k"] = 556,
+ ["l"] = 278,
+ ["m"] = 889,
+ ["n"] = 611,
+ ["o"] = 611,
+ ["p"] = 611,
+ ["q"] = 611,
+ ["r"] = 389,
+ ["s"] = 556,
+ ["t"] = 333,
+ ["u"] = 611,
+ ["v"] = 556,
+ ["w"] = 778,
+ ["x"] = 556,
+ ["y"] = 556,
+ ["z"] = 500,
+ ["braceleft"] = 389,
+ ["bar"] = 280,
+ ["braceright"] = 389,
+ ["asciitilde"] = 584,
+ ["exclamdown"] = 333,
+ ["cent"] = 556,
+ ["sterling"] = 556,
+ ["fraction"] = 167,
+ ["yen"] = 556,
+ ["florin"] = 556,
+ ["section"] = 556,
+ ["currency"] = 556,
+ ["quotesingle"] = 238,
+ ["quotedblleft"] = 500,
+ ["guillemotleft"] = 556,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 611,
+ ["fl"] = 611,
+ ["endash"] = 556,
+ ["dagger"] = 556,
+ ["daggerdbl"] = 556,
+ ["periodcentered"] = 278,
+ ["paragraph"] = 556,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 278,
+ ["quotedblbase"] = 500,
+ ["quotedblright"] = 500,
+ ["guillemotright"] = 556,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 611,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 1000,
+ ["ordfeminine"] = 370,
+ ["Lslash"] = 611,
+ ["Oslash"] = 778,
+ ["OE"] = 1000,
+ ["ordmasculine"] = 365,
+ ["ae"] = 889,
+ ["dotlessi"] = 278,
+ ["lslash"] = 278,
+ ["oslash"] = 611,
+ ["oe"] = 944,
+ ["germandbls"] = 611,
+ ["Idieresis"] = 278,
+ ["eacute"] = 556,
+ ["abreve"] = 556,
+ ["uhungarumlaut"] = 611,
+ ["ecaron"] = 556,
+ ["Ydieresis"] = 667,
+ ["divide"] = 584,
+ ["Yacute"] = 667,
+ ["Acircumflex"] = 722,
+ ["aacute"] = 556,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 556,
+ ["scommaaccent"] = 556,
+ ["ecircumflex"] = 556,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 556,
+ ["Uacute"] = 722,
+ ["uogonek"] = 611,
+ ["Edieresis"] = 667,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 737,
+ ["Emacron"] = 667,
+ ["ccaron"] = 556,
+ ["aring"] = 556,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 278,
+ ["agrave"] = 556,
+ ["Tcommaaccent"] = 611,
+ ["Cacute"] = 722,
+ ["atilde"] = 556,
+ ["Edotaccent"] = 667,
+ ["scaron"] = 556,
+ ["scedilla"] = 556,
+ ["iacute"] = 278,
+ ["lozenge"] = 494,
+ ["Rcaron"] = 722,
+ ["Gcommaaccent"] = 778,
+ ["ucircumflex"] = 611,
+ ["acircumflex"] = 556,
+ ["Amacron"] = 722,
+ ["rcaron"] = 389,
+ ["ccedilla"] = 556,
+ ["Zdotaccent"] = 611,
+ ["Thorn"] = 667,
+ ["Omacron"] = 778,
+ ["Racute"] = 722,
+ ["Sacute"] = 667,
+ ["dcaron"] = 743,
+ ["Umacron"] = 722,
+ ["uring"] = 611,
+ ["threesuperior"] = 333,
+ ["Ograve"] = 778,
+ ["Agrave"] = 722,
+ ["Abreve"] = 722,
+ ["multiply"] = 584,
+ ["uacute"] = 611,
+ ["Tcaron"] = 611,
+ ["partialdiff"] = 494,
+ ["ydieresis"] = 556,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 667,
+ ["adieresis"] = 556,
+ ["edieresis"] = 556,
+ ["cacute"] = 556,
+ ["nacute"] = 611,
+ ["umacron"] = 611,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 278,
+ ["plusminus"] = 584,
+ ["brokenbar"] = 280,
+ ["registered"] = 737,
+ ["Gbreve"] = 778,
+ ["Idotaccent"] = 278,
+ ["summation"] = 600,
+ ["Egrave"] = 667,
+ ["racute"] = 389,
+ ["omacron"] = 611,
+ ["Zacute"] = 611,
+ ["Zcaron"] = 611,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 722,
+ ["lcommaaccent"] = 278,
+ ["tcaron"] = 389,
+ ["eogonek"] = 556,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 722,
+ ["Adieresis"] = 722,
+ ["egrave"] = 556,
+ ["zacute"] = 500,
+ ["iogonek"] = 278,
+ ["Oacute"] = 778,
+ ["oacute"] = 611,
+ ["amacron"] = 556,
+ ["sacute"] = 556,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 778,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 611,
+ ["twosuperior"] = 333,
+ ["Odieresis"] = 778,
+ ["mu"] = 611,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 611,
+ ["Eogonek"] = 667,
+ ["dcroat"] = 611,
+ ["threequarters"] = 834,
+ ["Scedilla"] = 667,
+ ["lcaron"] = 400,
+ ["Kcommaaccent"] = 722,
+ ["Lacute"] = 611,
+ ["trademark"] = 1000,
+ ["edotaccent"] = 556,
+ ["Igrave"] = 278,
+ ["Imacron"] = 278,
+ ["Lcaron"] = 611,
+ ["onehalf"] = 834,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 611,
+ ["ntilde"] = 611,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 667,
+ ["emacron"] = 556,
+ ["gbreve"] = 611,
+ ["onequarter"] = 834,
+ ["Scaron"] = 667,
+ ["Scommaaccent"] = 667,
+ ["Ohungarumlaut"] = 778,
+ ["degree"] = 400,
+ ["ograve"] = 611,
+ ["Ccaron"] = 722,
+ ["ugrave"] = 611,
+ ["radical"] = 549,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 389,
+ ["Ntilde"] = 722,
+ ["otilde"] = 611,
+ ["Rcommaaccent"] = 722,
+ ["Lcommaaccent"] = 611,
+ ["Atilde"] = 722,
+ ["Aogonek"] = 722,
+ ["Aring"] = 722,
+ ["Otilde"] = 778,
+ ["zdotaccent"] = 500,
+ ["Ecaron"] = 667,
+ ["Iogonek"] = 278,
+ ["kcommaaccent"] = 556,
+ ["minus"] = 584,
+ ["Icircumflex"] = 278,
+ ["ncaron"] = 611,
+ ["tcommaaccent"] = 333,
+ ["logicalnot"] = 584,
+ ["odieresis"] = 611,
+ ["udieresis"] = 611,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 611,
+ ["eth"] = 611,
+ ["zcaron"] = 500,
+ ["ncommaaccent"] = 611,
+ ["onesuperior"] = 333,
+ ["imacron"] = 278,
+ ["Euro"] = 556,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _timesRomanWidths = new Dictionary
+ {
+ ["space"] = 250,
+ ["exclam"] = 333,
+ ["quotedbl"] = 408,
+ ["numbersign"] = 500,
+ ["dollar"] = 500,
+ ["percent"] = 833,
+ ["ampersand"] = 778,
+ ["quoteright"] = 333,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 500,
+ ["plus"] = 564,
+ ["comma"] = 250,
+ ["hyphen"] = 333,
+ ["period"] = 250,
+ ["slash"] = 278,
+ ["zero"] = 500,
+ ["one"] = 500,
+ ["two"] = 500,
+ ["three"] = 500,
+ ["four"] = 500,
+ ["five"] = 500,
+ ["six"] = 500,
+ ["seven"] = 500,
+ ["eight"] = 500,
+ ["nine"] = 500,
+ ["colon"] = 278,
+ ["semicolon"] = 278,
+ ["less"] = 564,
+ ["equal"] = 564,
+ ["greater"] = 564,
+ ["question"] = 444,
+ ["at"] = 921,
+ ["A"] = 722,
+ ["B"] = 667,
+ ["C"] = 667,
+ ["D"] = 722,
+ ["E"] = 611,
+ ["F"] = 556,
+ ["G"] = 722,
+ ["H"] = 722,
+ ["I"] = 333,
+ ["J"] = 389,
+ ["K"] = 722,
+ ["L"] = 611,
+ ["M"] = 889,
+ ["N"] = 722,
+ ["O"] = 722,
+ ["P"] = 556,
+ ["Q"] = 722,
+ ["R"] = 667,
+ ["S"] = 556,
+ ["T"] = 611,
+ ["U"] = 722,
+ ["V"] = 722,
+ ["W"] = 944,
+ ["X"] = 722,
+ ["Y"] = 722,
+ ["Z"] = 611,
+ ["bracketleft"] = 333,
+ ["backslash"] = 278,
+ ["bracketright"] = 333,
+ ["asciicircum"] = 469,
+ ["underscore"] = 500,
+ ["quoteleft"] = 333,
+ ["a"] = 444,
+ ["b"] = 500,
+ ["c"] = 444,
+ ["d"] = 500,
+ ["e"] = 444,
+ ["f"] = 333,
+ ["g"] = 500,
+ ["h"] = 500,
+ ["i"] = 278,
+ ["j"] = 278,
+ ["k"] = 500,
+ ["l"] = 278,
+ ["m"] = 778,
+ ["n"] = 500,
+ ["o"] = 500,
+ ["p"] = 500,
+ ["q"] = 500,
+ ["r"] = 333,
+ ["s"] = 389,
+ ["t"] = 278,
+ ["u"] = 500,
+ ["v"] = 500,
+ ["w"] = 722,
+ ["x"] = 500,
+ ["y"] = 500,
+ ["z"] = 444,
+ ["braceleft"] = 480,
+ ["bar"] = 200,
+ ["braceright"] = 480,
+ ["asciitilde"] = 541,
+ ["exclamdown"] = 333,
+ ["cent"] = 500,
+ ["sterling"] = 500,
+ ["fraction"] = 167,
+ ["yen"] = 500,
+ ["florin"] = 500,
+ ["section"] = 500,
+ ["currency"] = 500,
+ ["quotesingle"] = 180,
+ ["quotedblleft"] = 444,
+ ["guillemotleft"] = 500,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 556,
+ ["fl"] = 556,
+ ["endash"] = 500,
+ ["dagger"] = 500,
+ ["daggerdbl"] = 500,
+ ["periodcentered"] = 250,
+ ["paragraph"] = 453,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 333,
+ ["quotedblbase"] = 444,
+ ["quotedblright"] = 444,
+ ["guillemotright"] = 500,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 444,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 889,
+ ["ordfeminine"] = 276,
+ ["Lslash"] = 611,
+ ["Oslash"] = 722,
+ ["OE"] = 889,
+ ["ordmasculine"] = 310,
+ ["ae"] = 667,
+ ["dotlessi"] = 278,
+ ["lslash"] = 278,
+ ["oslash"] = 500,
+ ["oe"] = 722,
+ ["germandbls"] = 500,
+ ["Idieresis"] = 333,
+ ["eacute"] = 444,
+ ["abreve"] = 444,
+ ["uhungarumlaut"] = 500,
+ ["ecaron"] = 444,
+ ["Ydieresis"] = 722,
+ ["divide"] = 564,
+ ["Yacute"] = 722,
+ ["Acircumflex"] = 722,
+ ["aacute"] = 444,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 500,
+ ["scommaaccent"] = 389,
+ ["ecircumflex"] = 444,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 444,
+ ["Uacute"] = 722,
+ ["uogonek"] = 500,
+ ["Edieresis"] = 611,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 760,
+ ["Emacron"] = 611,
+ ["ccaron"] = 444,
+ ["aring"] = 444,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 278,
+ ["agrave"] = 444,
+ ["Tcommaaccent"] = 611,
+ ["Cacute"] = 667,
+ ["atilde"] = 444,
+ ["Edotaccent"] = 611,
+ ["scaron"] = 389,
+ ["scedilla"] = 389,
+ ["iacute"] = 278,
+ ["lozenge"] = 471,
+ ["Rcaron"] = 667,
+ ["Gcommaaccent"] = 722,
+ ["ucircumflex"] = 500,
+ ["acircumflex"] = 444,
+ ["Amacron"] = 722,
+ ["rcaron"] = 333,
+ ["ccedilla"] = 444,
+ ["Zdotaccent"] = 611,
+ ["Thorn"] = 556,
+ ["Omacron"] = 722,
+ ["Racute"] = 667,
+ ["Sacute"] = 556,
+ ["dcaron"] = 588,
+ ["Umacron"] = 722,
+ ["uring"] = 500,
+ ["threesuperior"] = 300,
+ ["Ograve"] = 722,
+ ["Agrave"] = 722,
+ ["Abreve"] = 722,
+ ["multiply"] = 564,
+ ["uacute"] = 500,
+ ["Tcaron"] = 611,
+ ["partialdiff"] = 476,
+ ["ydieresis"] = 500,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 611,
+ ["adieresis"] = 444,
+ ["edieresis"] = 444,
+ ["cacute"] = 444,
+ ["nacute"] = 500,
+ ["umacron"] = 500,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 333,
+ ["plusminus"] = 564,
+ ["brokenbar"] = 200,
+ ["registered"] = 760,
+ ["Gbreve"] = 722,
+ ["Idotaccent"] = 333,
+ ["summation"] = 600,
+ ["Egrave"] = 611,
+ ["racute"] = 333,
+ ["omacron"] = 500,
+ ["Zacute"] = 611,
+ ["Zcaron"] = 611,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 667,
+ ["lcommaaccent"] = 278,
+ ["tcaron"] = 326,
+ ["eogonek"] = 444,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 722,
+ ["Adieresis"] = 722,
+ ["egrave"] = 444,
+ ["zacute"] = 444,
+ ["iogonek"] = 278,
+ ["Oacute"] = 722,
+ ["oacute"] = 500,
+ ["amacron"] = 444,
+ ["sacute"] = 389,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 722,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 500,
+ ["twosuperior"] = 300,
+ ["Odieresis"] = 722,
+ ["mu"] = 500,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 500,
+ ["Eogonek"] = 611,
+ ["dcroat"] = 500,
+ ["threequarters"] = 750,
+ ["Scedilla"] = 556,
+ ["lcaron"] = 344,
+ ["Kcommaaccent"] = 722,
+ ["Lacute"] = 611,
+ ["trademark"] = 980,
+ ["edotaccent"] = 444,
+ ["Igrave"] = 333,
+ ["Imacron"] = 333,
+ ["Lcaron"] = 611,
+ ["onehalf"] = 750,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 500,
+ ["ntilde"] = 500,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 611,
+ ["emacron"] = 444,
+ ["gbreve"] = 500,
+ ["onequarter"] = 750,
+ ["Scaron"] = 556,
+ ["Scommaaccent"] = 556,
+ ["Ohungarumlaut"] = 722,
+ ["degree"] = 400,
+ ["ograve"] = 500,
+ ["Ccaron"] = 667,
+ ["ugrave"] = 500,
+ ["radical"] = 453,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 333,
+ ["Ntilde"] = 722,
+ ["otilde"] = 500,
+ ["Rcommaaccent"] = 667,
+ ["Lcommaaccent"] = 611,
+ ["Atilde"] = 722,
+ ["Aogonek"] = 722,
+ ["Aring"] = 722,
+ ["Otilde"] = 722,
+ ["zdotaccent"] = 444,
+ ["Ecaron"] = 611,
+ ["Iogonek"] = 333,
+ ["kcommaaccent"] = 500,
+ ["minus"] = 564,
+ ["Icircumflex"] = 333,
+ ["ncaron"] = 500,
+ ["tcommaaccent"] = 278,
+ ["logicalnot"] = 564,
+ ["odieresis"] = 500,
+ ["udieresis"] = 500,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 500,
+ ["eth"] = 500,
+ ["zcaron"] = 444,
+ ["ncommaaccent"] = 500,
+ ["onesuperior"] = 300,
+ ["imacron"] = 278,
+ ["Euro"] = 500,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _timesBoldWidths = new Dictionary
+ {
+ ["space"] = 250,
+ ["exclam"] = 333,
+ ["quotedbl"] = 555,
+ ["numbersign"] = 500,
+ ["dollar"] = 500,
+ ["percent"] = 1000,
+ ["ampersand"] = 833,
+ ["quoteright"] = 333,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 500,
+ ["plus"] = 570,
+ ["comma"] = 250,
+ ["hyphen"] = 333,
+ ["period"] = 250,
+ ["slash"] = 278,
+ ["zero"] = 500,
+ ["one"] = 500,
+ ["two"] = 500,
+ ["three"] = 500,
+ ["four"] = 500,
+ ["five"] = 500,
+ ["six"] = 500,
+ ["seven"] = 500,
+ ["eight"] = 500,
+ ["nine"] = 500,
+ ["colon"] = 333,
+ ["semicolon"] = 333,
+ ["less"] = 570,
+ ["equal"] = 570,
+ ["greater"] = 570,
+ ["question"] = 500,
+ ["at"] = 930,
+ ["A"] = 722,
+ ["B"] = 667,
+ ["C"] = 722,
+ ["D"] = 722,
+ ["E"] = 667,
+ ["F"] = 611,
+ ["G"] = 778,
+ ["H"] = 778,
+ ["I"] = 389,
+ ["J"] = 500,
+ ["K"] = 778,
+ ["L"] = 667,
+ ["M"] = 944,
+ ["N"] = 722,
+ ["O"] = 778,
+ ["P"] = 611,
+ ["Q"] = 778,
+ ["R"] = 722,
+ ["S"] = 556,
+ ["T"] = 667,
+ ["U"] = 722,
+ ["V"] = 722,
+ ["W"] = 1000,
+ ["X"] = 722,
+ ["Y"] = 722,
+ ["Z"] = 667,
+ ["bracketleft"] = 333,
+ ["backslash"] = 278,
+ ["bracketright"] = 333,
+ ["asciicircum"] = 581,
+ ["underscore"] = 500,
+ ["quoteleft"] = 333,
+ ["a"] = 500,
+ ["b"] = 556,
+ ["c"] = 444,
+ ["d"] = 556,
+ ["e"] = 444,
+ ["f"] = 333,
+ ["g"] = 500,
+ ["h"] = 556,
+ ["i"] = 278,
+ ["j"] = 333,
+ ["k"] = 556,
+ ["l"] = 278,
+ ["m"] = 833,
+ ["n"] = 556,
+ ["o"] = 500,
+ ["p"] = 556,
+ ["q"] = 556,
+ ["r"] = 444,
+ ["s"] = 389,
+ ["t"] = 333,
+ ["u"] = 556,
+ ["v"] = 500,
+ ["w"] = 722,
+ ["x"] = 500,
+ ["y"] = 500,
+ ["z"] = 444,
+ ["braceleft"] = 394,
+ ["bar"] = 220,
+ ["braceright"] = 394,
+ ["asciitilde"] = 520,
+ ["exclamdown"] = 333,
+ ["cent"] = 500,
+ ["sterling"] = 500,
+ ["fraction"] = 167,
+ ["yen"] = 500,
+ ["florin"] = 500,
+ ["section"] = 500,
+ ["currency"] = 500,
+ ["quotesingle"] = 278,
+ ["quotedblleft"] = 500,
+ ["guillemotleft"] = 500,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 556,
+ ["fl"] = 556,
+ ["endash"] = 500,
+ ["dagger"] = 500,
+ ["daggerdbl"] = 500,
+ ["periodcentered"] = 250,
+ ["paragraph"] = 540,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 333,
+ ["quotedblbase"] = 500,
+ ["quotedblright"] = 500,
+ ["guillemotright"] = 500,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 500,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 1000,
+ ["ordfeminine"] = 300,
+ ["Lslash"] = 667,
+ ["Oslash"] = 778,
+ ["OE"] = 1000,
+ ["ordmasculine"] = 330,
+ ["ae"] = 722,
+ ["dotlessi"] = 278,
+ ["lslash"] = 278,
+ ["oslash"] = 500,
+ ["oe"] = 722,
+ ["germandbls"] = 556,
+ ["Idieresis"] = 389,
+ ["eacute"] = 444,
+ ["abreve"] = 500,
+ ["uhungarumlaut"] = 556,
+ ["ecaron"] = 444,
+ ["Ydieresis"] = 722,
+ ["divide"] = 570,
+ ["Yacute"] = 722,
+ ["Acircumflex"] = 722,
+ ["aacute"] = 500,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 500,
+ ["scommaaccent"] = 389,
+ ["ecircumflex"] = 444,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 500,
+ ["Uacute"] = 722,
+ ["uogonek"] = 556,
+ ["Edieresis"] = 667,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 747,
+ ["Emacron"] = 667,
+ ["ccaron"] = 444,
+ ["aring"] = 500,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 278,
+ ["agrave"] = 500,
+ ["Tcommaaccent"] = 667,
+ ["Cacute"] = 722,
+ ["atilde"] = 500,
+ ["Edotaccent"] = 667,
+ ["scaron"] = 389,
+ ["scedilla"] = 389,
+ ["iacute"] = 278,
+ ["lozenge"] = 494,
+ ["Rcaron"] = 722,
+ ["Gcommaaccent"] = 778,
+ ["ucircumflex"] = 556,
+ ["acircumflex"] = 500,
+ ["Amacron"] = 722,
+ ["rcaron"] = 444,
+ ["ccedilla"] = 444,
+ ["Zdotaccent"] = 667,
+ ["Thorn"] = 611,
+ ["Omacron"] = 778,
+ ["Racute"] = 722,
+ ["Sacute"] = 556,
+ ["dcaron"] = 672,
+ ["Umacron"] = 722,
+ ["uring"] = 556,
+ ["threesuperior"] = 300,
+ ["Ograve"] = 778,
+ ["Agrave"] = 722,
+ ["Abreve"] = 722,
+ ["multiply"] = 570,
+ ["uacute"] = 556,
+ ["Tcaron"] = 667,
+ ["partialdiff"] = 494,
+ ["ydieresis"] = 500,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 667,
+ ["adieresis"] = 500,
+ ["edieresis"] = 444,
+ ["cacute"] = 444,
+ ["nacute"] = 556,
+ ["umacron"] = 556,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 389,
+ ["plusminus"] = 570,
+ ["brokenbar"] = 220,
+ ["registered"] = 747,
+ ["Gbreve"] = 778,
+ ["Idotaccent"] = 389,
+ ["summation"] = 600,
+ ["Egrave"] = 667,
+ ["racute"] = 444,
+ ["omacron"] = 500,
+ ["Zacute"] = 667,
+ ["Zcaron"] = 667,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 722,
+ ["lcommaaccent"] = 278,
+ ["tcaron"] = 416,
+ ["eogonek"] = 444,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 722,
+ ["Adieresis"] = 722,
+ ["egrave"] = 444,
+ ["zacute"] = 444,
+ ["iogonek"] = 278,
+ ["Oacute"] = 778,
+ ["oacute"] = 500,
+ ["amacron"] = 500,
+ ["sacute"] = 389,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 778,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 556,
+ ["twosuperior"] = 300,
+ ["Odieresis"] = 778,
+ ["mu"] = 556,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 500,
+ ["Eogonek"] = 667,
+ ["dcroat"] = 556,
+ ["threequarters"] = 750,
+ ["Scedilla"] = 556,
+ ["lcaron"] = 394,
+ ["Kcommaaccent"] = 778,
+ ["Lacute"] = 667,
+ ["trademark"] = 1000,
+ ["edotaccent"] = 444,
+ ["Igrave"] = 389,
+ ["Imacron"] = 389,
+ ["Lcaron"] = 667,
+ ["onehalf"] = 750,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 500,
+ ["ntilde"] = 556,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 667,
+ ["emacron"] = 444,
+ ["gbreve"] = 500,
+ ["onequarter"] = 750,
+ ["Scaron"] = 556,
+ ["Scommaaccent"] = 556,
+ ["Ohungarumlaut"] = 778,
+ ["degree"] = 400,
+ ["ograve"] = 500,
+ ["Ccaron"] = 722,
+ ["ugrave"] = 556,
+ ["radical"] = 549,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 444,
+ ["Ntilde"] = 722,
+ ["otilde"] = 500,
+ ["Rcommaaccent"] = 722,
+ ["Lcommaaccent"] = 667,
+ ["Atilde"] = 722,
+ ["Aogonek"] = 722,
+ ["Aring"] = 722,
+ ["Otilde"] = 778,
+ ["zdotaccent"] = 444,
+ ["Ecaron"] = 667,
+ ["Iogonek"] = 389,
+ ["kcommaaccent"] = 556,
+ ["minus"] = 570,
+ ["Icircumflex"] = 389,
+ ["ncaron"] = 556,
+ ["tcommaaccent"] = 333,
+ ["logicalnot"] = 570,
+ ["odieresis"] = 500,
+ ["udieresis"] = 556,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 500,
+ ["eth"] = 500,
+ ["zcaron"] = 444,
+ ["ncommaaccent"] = 556,
+ ["onesuperior"] = 300,
+ ["imacron"] = 278,
+ ["Euro"] = 500,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _timesItalicWidths = new Dictionary
+ {
+ ["space"] = 250,
+ ["exclam"] = 333,
+ ["quotedbl"] = 420,
+ ["numbersign"] = 500,
+ ["dollar"] = 500,
+ ["percent"] = 833,
+ ["ampersand"] = 778,
+ ["quoteright"] = 333,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 500,
+ ["plus"] = 675,
+ ["comma"] = 250,
+ ["hyphen"] = 333,
+ ["period"] = 250,
+ ["slash"] = 278,
+ ["zero"] = 500,
+ ["one"] = 500,
+ ["two"] = 500,
+ ["three"] = 500,
+ ["four"] = 500,
+ ["five"] = 500,
+ ["six"] = 500,
+ ["seven"] = 500,
+ ["eight"] = 500,
+ ["nine"] = 500,
+ ["colon"] = 333,
+ ["semicolon"] = 333,
+ ["less"] = 675,
+ ["equal"] = 675,
+ ["greater"] = 675,
+ ["question"] = 500,
+ ["at"] = 920,
+ ["A"] = 611,
+ ["B"] = 611,
+ ["C"] = 667,
+ ["D"] = 722,
+ ["E"] = 611,
+ ["F"] = 611,
+ ["G"] = 722,
+ ["H"] = 722,
+ ["I"] = 333,
+ ["J"] = 444,
+ ["K"] = 667,
+ ["L"] = 556,
+ ["M"] = 833,
+ ["N"] = 667,
+ ["O"] = 722,
+ ["P"] = 611,
+ ["Q"] = 722,
+ ["R"] = 611,
+ ["S"] = 500,
+ ["T"] = 556,
+ ["U"] = 722,
+ ["V"] = 611,
+ ["W"] = 833,
+ ["X"] = 611,
+ ["Y"] = 556,
+ ["Z"] = 556,
+ ["bracketleft"] = 389,
+ ["backslash"] = 278,
+ ["bracketright"] = 389,
+ ["asciicircum"] = 422,
+ ["underscore"] = 500,
+ ["quoteleft"] = 333,
+ ["a"] = 500,
+ ["b"] = 500,
+ ["c"] = 444,
+ ["d"] = 500,
+ ["e"] = 444,
+ ["f"] = 278,
+ ["g"] = 500,
+ ["h"] = 500,
+ ["i"] = 278,
+ ["j"] = 278,
+ ["k"] = 444,
+ ["l"] = 278,
+ ["m"] = 722,
+ ["n"] = 500,
+ ["o"] = 500,
+ ["p"] = 500,
+ ["q"] = 500,
+ ["r"] = 389,
+ ["s"] = 389,
+ ["t"] = 278,
+ ["u"] = 500,
+ ["v"] = 444,
+ ["w"] = 667,
+ ["x"] = 444,
+ ["y"] = 444,
+ ["z"] = 389,
+ ["braceleft"] = 400,
+ ["bar"] = 275,
+ ["braceright"] = 400,
+ ["asciitilde"] = 541,
+ ["exclamdown"] = 389,
+ ["cent"] = 500,
+ ["sterling"] = 500,
+ ["fraction"] = 167,
+ ["yen"] = 500,
+ ["florin"] = 500,
+ ["section"] = 500,
+ ["currency"] = 500,
+ ["quotesingle"] = 214,
+ ["quotedblleft"] = 556,
+ ["guillemotleft"] = 500,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 500,
+ ["fl"] = 500,
+ ["endash"] = 500,
+ ["dagger"] = 500,
+ ["daggerdbl"] = 500,
+ ["periodcentered"] = 250,
+ ["paragraph"] = 523,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 333,
+ ["quotedblbase"] = 556,
+ ["quotedblright"] = 556,
+ ["guillemotright"] = 500,
+ ["ellipsis"] = 889,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 500,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 889,
+ ["AE"] = 889,
+ ["ordfeminine"] = 276,
+ ["Lslash"] = 556,
+ ["Oslash"] = 722,
+ ["OE"] = 944,
+ ["ordmasculine"] = 310,
+ ["ae"] = 667,
+ ["dotlessi"] = 278,
+ ["lslash"] = 278,
+ ["oslash"] = 500,
+ ["oe"] = 667,
+ ["germandbls"] = 500,
+ ["Idieresis"] = 333,
+ ["eacute"] = 444,
+ ["abreve"] = 500,
+ ["uhungarumlaut"] = 500,
+ ["ecaron"] = 444,
+ ["Ydieresis"] = 556,
+ ["divide"] = 675,
+ ["Yacute"] = 556,
+ ["Acircumflex"] = 611,
+ ["aacute"] = 500,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 444,
+ ["scommaaccent"] = 389,
+ ["ecircumflex"] = 444,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 500,
+ ["Uacute"] = 722,
+ ["uogonek"] = 500,
+ ["Edieresis"] = 611,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 760,
+ ["Emacron"] = 611,
+ ["ccaron"] = 444,
+ ["aring"] = 500,
+ ["Ncommaaccent"] = 667,
+ ["lacute"] = 278,
+ ["agrave"] = 500,
+ ["Tcommaaccent"] = 556,
+ ["Cacute"] = 667,
+ ["atilde"] = 500,
+ ["Edotaccent"] = 611,
+ ["scaron"] = 389,
+ ["scedilla"] = 389,
+ ["iacute"] = 278,
+ ["lozenge"] = 471,
+ ["Rcaron"] = 611,
+ ["Gcommaaccent"] = 722,
+ ["ucircumflex"] = 500,
+ ["acircumflex"] = 500,
+ ["Amacron"] = 611,
+ ["rcaron"] = 389,
+ ["ccedilla"] = 444,
+ ["Zdotaccent"] = 556,
+ ["Thorn"] = 611,
+ ["Omacron"] = 722,
+ ["Racute"] = 611,
+ ["Sacute"] = 500,
+ ["dcaron"] = 544,
+ ["Umacron"] = 722,
+ ["uring"] = 500,
+ ["threesuperior"] = 300,
+ ["Ograve"] = 722,
+ ["Agrave"] = 611,
+ ["Abreve"] = 611,
+ ["multiply"] = 675,
+ ["uacute"] = 500,
+ ["Tcaron"] = 556,
+ ["partialdiff"] = 476,
+ ["ydieresis"] = 444,
+ ["Nacute"] = 667,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 611,
+ ["adieresis"] = 500,
+ ["edieresis"] = 444,
+ ["cacute"] = 444,
+ ["nacute"] = 500,
+ ["umacron"] = 500,
+ ["Ncaron"] = 667,
+ ["Iacute"] = 333,
+ ["plusminus"] = 675,
+ ["brokenbar"] = 275,
+ ["registered"] = 760,
+ ["Gbreve"] = 722,
+ ["Idotaccent"] = 333,
+ ["summation"] = 600,
+ ["Egrave"] = 611,
+ ["racute"] = 389,
+ ["omacron"] = 500,
+ ["Zacute"] = 556,
+ ["Zcaron"] = 556,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 667,
+ ["lcommaaccent"] = 278,
+ ["tcaron"] = 300,
+ ["eogonek"] = 444,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 611,
+ ["Adieresis"] = 611,
+ ["egrave"] = 444,
+ ["zacute"] = 389,
+ ["iogonek"] = 278,
+ ["Oacute"] = 722,
+ ["oacute"] = 500,
+ ["amacron"] = 500,
+ ["sacute"] = 389,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 722,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 500,
+ ["twosuperior"] = 300,
+ ["Odieresis"] = 722,
+ ["mu"] = 500,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 500,
+ ["Eogonek"] = 611,
+ ["dcroat"] = 500,
+ ["threequarters"] = 750,
+ ["Scedilla"] = 500,
+ ["lcaron"] = 300,
+ ["Kcommaaccent"] = 667,
+ ["Lacute"] = 556,
+ ["trademark"] = 980,
+ ["edotaccent"] = 444,
+ ["Igrave"] = 333,
+ ["Imacron"] = 333,
+ ["Lcaron"] = 611,
+ ["onehalf"] = 750,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 500,
+ ["ntilde"] = 500,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 611,
+ ["emacron"] = 444,
+ ["gbreve"] = 500,
+ ["onequarter"] = 750,
+ ["Scaron"] = 500,
+ ["Scommaaccent"] = 500,
+ ["Ohungarumlaut"] = 722,
+ ["degree"] = 400,
+ ["ograve"] = 500,
+ ["Ccaron"] = 667,
+ ["ugrave"] = 500,
+ ["radical"] = 453,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 389,
+ ["Ntilde"] = 667,
+ ["otilde"] = 500,
+ ["Rcommaaccent"] = 611,
+ ["Lcommaaccent"] = 556,
+ ["Atilde"] = 611,
+ ["Aogonek"] = 611,
+ ["Aring"] = 611,
+ ["Otilde"] = 722,
+ ["zdotaccent"] = 389,
+ ["Ecaron"] = 611,
+ ["Iogonek"] = 333,
+ ["kcommaaccent"] = 444,
+ ["minus"] = 675,
+ ["Icircumflex"] = 333,
+ ["ncaron"] = 500,
+ ["tcommaaccent"] = 278,
+ ["logicalnot"] = 675,
+ ["odieresis"] = 500,
+ ["udieresis"] = 500,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 500,
+ ["eth"] = 500,
+ ["zcaron"] = 389,
+ ["ncommaaccent"] = 500,
+ ["onesuperior"] = 300,
+ ["imacron"] = 278,
+ ["Euro"] = 500,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _timesBoldItalicWidths = new Dictionary
+ {
+ ["space"] = 250,
+ ["exclam"] = 389,
+ ["quotedbl"] = 555,
+ ["numbersign"] = 500,
+ ["dollar"] = 500,
+ ["percent"] = 833,
+ ["ampersand"] = 778,
+ ["quoteright"] = 333,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asterisk"] = 500,
+ ["plus"] = 570,
+ ["comma"] = 250,
+ ["hyphen"] = 333,
+ ["period"] = 250,
+ ["slash"] = 278,
+ ["zero"] = 500,
+ ["one"] = 500,
+ ["two"] = 500,
+ ["three"] = 500,
+ ["four"] = 500,
+ ["five"] = 500,
+ ["six"] = 500,
+ ["seven"] = 500,
+ ["eight"] = 500,
+ ["nine"] = 500,
+ ["colon"] = 333,
+ ["semicolon"] = 333,
+ ["less"] = 570,
+ ["equal"] = 570,
+ ["greater"] = 570,
+ ["question"] = 500,
+ ["at"] = 832,
+ ["A"] = 667,
+ ["B"] = 667,
+ ["C"] = 667,
+ ["D"] = 722,
+ ["E"] = 667,
+ ["F"] = 667,
+ ["G"] = 722,
+ ["H"] = 778,
+ ["I"] = 389,
+ ["J"] = 500,
+ ["K"] = 667,
+ ["L"] = 611,
+ ["M"] = 889,
+ ["N"] = 722,
+ ["O"] = 722,
+ ["P"] = 611,
+ ["Q"] = 722,
+ ["R"] = 667,
+ ["S"] = 556,
+ ["T"] = 611,
+ ["U"] = 722,
+ ["V"] = 667,
+ ["W"] = 889,
+ ["X"] = 667,
+ ["Y"] = 611,
+ ["Z"] = 611,
+ ["bracketleft"] = 333,
+ ["backslash"] = 278,
+ ["bracketright"] = 333,
+ ["asciicircum"] = 570,
+ ["underscore"] = 500,
+ ["quoteleft"] = 333,
+ ["a"] = 500,
+ ["b"] = 500,
+ ["c"] = 444,
+ ["d"] = 500,
+ ["e"] = 444,
+ ["f"] = 333,
+ ["g"] = 500,
+ ["h"] = 556,
+ ["i"] = 278,
+ ["j"] = 278,
+ ["k"] = 500,
+ ["l"] = 278,
+ ["m"] = 778,
+ ["n"] = 556,
+ ["o"] = 500,
+ ["p"] = 500,
+ ["q"] = 500,
+ ["r"] = 389,
+ ["s"] = 389,
+ ["t"] = 278,
+ ["u"] = 556,
+ ["v"] = 444,
+ ["w"] = 667,
+ ["x"] = 500,
+ ["y"] = 444,
+ ["z"] = 389,
+ ["braceleft"] = 348,
+ ["bar"] = 220,
+ ["braceright"] = 348,
+ ["asciitilde"] = 570,
+ ["exclamdown"] = 389,
+ ["cent"] = 500,
+ ["sterling"] = 500,
+ ["fraction"] = 167,
+ ["yen"] = 500,
+ ["florin"] = 500,
+ ["section"] = 500,
+ ["currency"] = 500,
+ ["quotesingle"] = 278,
+ ["quotedblleft"] = 500,
+ ["guillemotleft"] = 500,
+ ["guilsinglleft"] = 333,
+ ["guilsinglright"] = 333,
+ ["fi"] = 556,
+ ["fl"] = 556,
+ ["endash"] = 500,
+ ["dagger"] = 500,
+ ["daggerdbl"] = 500,
+ ["periodcentered"] = 250,
+ ["paragraph"] = 500,
+ ["bullet"] = 350,
+ ["quotesinglbase"] = 333,
+ ["quotedblbase"] = 500,
+ ["quotedblright"] = 500,
+ ["guillemotright"] = 500,
+ ["ellipsis"] = 1000,
+ ["perthousand"] = 1000,
+ ["questiondown"] = 500,
+ ["grave"] = 333,
+ ["acute"] = 333,
+ ["circumflex"] = 333,
+ ["tilde"] = 333,
+ ["macron"] = 333,
+ ["breve"] = 333,
+ ["dotaccent"] = 333,
+ ["dieresis"] = 333,
+ ["ring"] = 333,
+ ["cedilla"] = 333,
+ ["hungarumlaut"] = 333,
+ ["ogonek"] = 333,
+ ["caron"] = 333,
+ ["emdash"] = 1000,
+ ["AE"] = 944,
+ ["ordfeminine"] = 266,
+ ["Lslash"] = 611,
+ ["Oslash"] = 722,
+ ["OE"] = 944,
+ ["ordmasculine"] = 300,
+ ["ae"] = 722,
+ ["dotlessi"] = 278,
+ ["lslash"] = 278,
+ ["oslash"] = 500,
+ ["oe"] = 722,
+ ["germandbls"] = 500,
+ ["Idieresis"] = 389,
+ ["eacute"] = 444,
+ ["abreve"] = 500,
+ ["uhungarumlaut"] = 556,
+ ["ecaron"] = 444,
+ ["Ydieresis"] = 611,
+ ["divide"] = 570,
+ ["Yacute"] = 611,
+ ["Acircumflex"] = 667,
+ ["aacute"] = 500,
+ ["Ucircumflex"] = 722,
+ ["yacute"] = 444,
+ ["scommaaccent"] = 389,
+ ["ecircumflex"] = 444,
+ ["Uring"] = 722,
+ ["Udieresis"] = 722,
+ ["aogonek"] = 500,
+ ["Uacute"] = 722,
+ ["uogonek"] = 556,
+ ["Edieresis"] = 667,
+ ["Dcroat"] = 722,
+ ["commaaccent"] = 250,
+ ["copyright"] = 747,
+ ["Emacron"] = 667,
+ ["ccaron"] = 444,
+ ["aring"] = 500,
+ ["Ncommaaccent"] = 722,
+ ["lacute"] = 278,
+ ["agrave"] = 500,
+ ["Tcommaaccent"] = 611,
+ ["Cacute"] = 667,
+ ["atilde"] = 500,
+ ["Edotaccent"] = 667,
+ ["scaron"] = 389,
+ ["scedilla"] = 389,
+ ["iacute"] = 278,
+ ["lozenge"] = 494,
+ ["Rcaron"] = 667,
+ ["Gcommaaccent"] = 722,
+ ["ucircumflex"] = 556,
+ ["acircumflex"] = 500,
+ ["Amacron"] = 667,
+ ["rcaron"] = 389,
+ ["ccedilla"] = 444,
+ ["Zdotaccent"] = 611,
+ ["Thorn"] = 611,
+ ["Omacron"] = 722,
+ ["Racute"] = 667,
+ ["Sacute"] = 556,
+ ["dcaron"] = 608,
+ ["Umacron"] = 722,
+ ["uring"] = 556,
+ ["threesuperior"] = 300,
+ ["Ograve"] = 722,
+ ["Agrave"] = 667,
+ ["Abreve"] = 667,
+ ["multiply"] = 570,
+ ["uacute"] = 556,
+ ["Tcaron"] = 611,
+ ["partialdiff"] = 494,
+ ["ydieresis"] = 444,
+ ["Nacute"] = 722,
+ ["icircumflex"] = 278,
+ ["Ecircumflex"] = 667,
+ ["adieresis"] = 500,
+ ["edieresis"] = 444,
+ ["cacute"] = 444,
+ ["nacute"] = 556,
+ ["umacron"] = 556,
+ ["Ncaron"] = 722,
+ ["Iacute"] = 389,
+ ["plusminus"] = 570,
+ ["brokenbar"] = 220,
+ ["registered"] = 747,
+ ["Gbreve"] = 722,
+ ["Idotaccent"] = 389,
+ ["summation"] = 600,
+ ["Egrave"] = 667,
+ ["racute"] = 389,
+ ["omacron"] = 500,
+ ["Zacute"] = 611,
+ ["Zcaron"] = 611,
+ ["greaterequal"] = 549,
+ ["Eth"] = 722,
+ ["Ccedilla"] = 667,
+ ["lcommaaccent"] = 278,
+ ["tcaron"] = 366,
+ ["eogonek"] = 444,
+ ["Uogonek"] = 722,
+ ["Aacute"] = 667,
+ ["Adieresis"] = 667,
+ ["egrave"] = 444,
+ ["zacute"] = 389,
+ ["iogonek"] = 278,
+ ["Oacute"] = 722,
+ ["oacute"] = 500,
+ ["amacron"] = 500,
+ ["sacute"] = 389,
+ ["idieresis"] = 278,
+ ["Ocircumflex"] = 722,
+ ["Ugrave"] = 722,
+ ["Delta"] = 612,
+ ["thorn"] = 500,
+ ["twosuperior"] = 300,
+ ["Odieresis"] = 722,
+ ["mu"] = 576,
+ ["igrave"] = 278,
+ ["ohungarumlaut"] = 500,
+ ["Eogonek"] = 667,
+ ["dcroat"] = 500,
+ ["threequarters"] = 750,
+ ["Scedilla"] = 556,
+ ["lcaron"] = 382,
+ ["Kcommaaccent"] = 667,
+ ["Lacute"] = 611,
+ ["trademark"] = 1000,
+ ["edotaccent"] = 444,
+ ["Igrave"] = 389,
+ ["Imacron"] = 389,
+ ["Lcaron"] = 611,
+ ["onehalf"] = 750,
+ ["lessequal"] = 549,
+ ["ocircumflex"] = 500,
+ ["ntilde"] = 556,
+ ["Uhungarumlaut"] = 722,
+ ["Eacute"] = 667,
+ ["emacron"] = 444,
+ ["gbreve"] = 500,
+ ["onequarter"] = 750,
+ ["Scaron"] = 556,
+ ["Scommaaccent"] = 556,
+ ["Ohungarumlaut"] = 722,
+ ["degree"] = 400,
+ ["ograve"] = 500,
+ ["Ccaron"] = 667,
+ ["ugrave"] = 556,
+ ["radical"] = 549,
+ ["Dcaron"] = 722,
+ ["rcommaaccent"] = 389,
+ ["Ntilde"] = 722,
+ ["otilde"] = 500,
+ ["Rcommaaccent"] = 667,
+ ["Lcommaaccent"] = 611,
+ ["Atilde"] = 667,
+ ["Aogonek"] = 667,
+ ["Aring"] = 667,
+ ["Otilde"] = 722,
+ ["zdotaccent"] = 389,
+ ["Ecaron"] = 667,
+ ["Iogonek"] = 389,
+ ["kcommaaccent"] = 500,
+ ["minus"] = 606,
+ ["Icircumflex"] = 389,
+ ["ncaron"] = 556,
+ ["tcommaaccent"] = 278,
+ ["logicalnot"] = 606,
+ ["odieresis"] = 500,
+ ["udieresis"] = 556,
+ ["notequal"] = 549,
+ ["gcommaaccent"] = 500,
+ ["eth"] = 500,
+ ["zcaron"] = 389,
+ ["ncommaaccent"] = 556,
+ ["onesuperior"] = 300,
+ ["imacron"] = 278,
+ ["Euro"] = 500,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _courierWidths = new Dictionary
+ {
+ ["space"] = 600,
+ ["exclam"] = 600,
+ ["quotedbl"] = 600,
+ ["numbersign"] = 600,
+ ["dollar"] = 600,
+ ["percent"] = 600,
+ ["ampersand"] = 600,
+ ["quoteright"] = 600,
+ ["parenleft"] = 600,
+ ["parenright"] = 600,
+ ["asterisk"] = 600,
+ ["plus"] = 600,
+ ["comma"] = 600,
+ ["hyphen"] = 600,
+ ["period"] = 600,
+ ["slash"] = 600,
+ ["zero"] = 600,
+ ["one"] = 600,
+ ["two"] = 600,
+ ["three"] = 600,
+ ["four"] = 600,
+ ["five"] = 600,
+ ["six"] = 600,
+ ["seven"] = 600,
+ ["eight"] = 600,
+ ["nine"] = 600,
+ ["colon"] = 600,
+ ["semicolon"] = 600,
+ ["less"] = 600,
+ ["equal"] = 600,
+ ["greater"] = 600,
+ ["question"] = 600,
+ ["at"] = 600,
+ ["A"] = 600,
+ ["B"] = 600,
+ ["C"] = 600,
+ ["D"] = 600,
+ ["E"] = 600,
+ ["F"] = 600,
+ ["G"] = 600,
+ ["H"] = 600,
+ ["I"] = 600,
+ ["J"] = 600,
+ ["K"] = 600,
+ ["L"] = 600,
+ ["M"] = 600,
+ ["N"] = 600,
+ ["O"] = 600,
+ ["P"] = 600,
+ ["Q"] = 600,
+ ["R"] = 600,
+ ["S"] = 600,
+ ["T"] = 600,
+ ["U"] = 600,
+ ["V"] = 600,
+ ["W"] = 600,
+ ["X"] = 600,
+ ["Y"] = 600,
+ ["Z"] = 600,
+ ["bracketleft"] = 600,
+ ["backslash"] = 600,
+ ["bracketright"] = 600,
+ ["asciicircum"] = 600,
+ ["underscore"] = 600,
+ ["quoteleft"] = 600,
+ ["a"] = 600,
+ ["b"] = 600,
+ ["c"] = 600,
+ ["d"] = 600,
+ ["e"] = 600,
+ ["f"] = 600,
+ ["g"] = 600,
+ ["h"] = 600,
+ ["i"] = 600,
+ ["j"] = 600,
+ ["k"] = 600,
+ ["l"] = 600,
+ ["m"] = 600,
+ ["n"] = 600,
+ ["o"] = 600,
+ ["p"] = 600,
+ ["q"] = 600,
+ ["r"] = 600,
+ ["s"] = 600,
+ ["t"] = 600,
+ ["u"] = 600,
+ ["v"] = 600,
+ ["w"] = 600,
+ ["x"] = 600,
+ ["y"] = 600,
+ ["z"] = 600,
+ ["braceleft"] = 600,
+ ["bar"] = 600,
+ ["braceright"] = 600,
+ ["asciitilde"] = 600,
+ ["exclamdown"] = 600,
+ ["cent"] = 600,
+ ["sterling"] = 600,
+ ["fraction"] = 600,
+ ["yen"] = 600,
+ ["florin"] = 600,
+ ["section"] = 600,
+ ["currency"] = 600,
+ ["quotesingle"] = 600,
+ ["quotedblleft"] = 600,
+ ["guillemotleft"] = 600,
+ ["guilsinglleft"] = 600,
+ ["guilsinglright"] = 600,
+ ["fi"] = 600,
+ ["fl"] = 600,
+ ["endash"] = 600,
+ ["dagger"] = 600,
+ ["daggerdbl"] = 600,
+ ["periodcentered"] = 600,
+ ["paragraph"] = 600,
+ ["bullet"] = 600,
+ ["quotesinglbase"] = 600,
+ ["quotedblbase"] = 600,
+ ["quotedblright"] = 600,
+ ["guillemotright"] = 600,
+ ["ellipsis"] = 600,
+ ["perthousand"] = 600,
+ ["questiondown"] = 600,
+ ["grave"] = 600,
+ ["acute"] = 600,
+ ["circumflex"] = 600,
+ ["tilde"] = 600,
+ ["macron"] = 600,
+ ["breve"] = 600,
+ ["dotaccent"] = 600,
+ ["dieresis"] = 600,
+ ["ring"] = 600,
+ ["cedilla"] = 600,
+ ["hungarumlaut"] = 600,
+ ["ogonek"] = 600,
+ ["caron"] = 600,
+ ["emdash"] = 600,
+ ["AE"] = 600,
+ ["ordfeminine"] = 600,
+ ["Lslash"] = 600,
+ ["Oslash"] = 600,
+ ["OE"] = 600,
+ ["ordmasculine"] = 600,
+ ["ae"] = 600,
+ ["dotlessi"] = 600,
+ ["lslash"] = 600,
+ ["oslash"] = 600,
+ ["oe"] = 600,
+ ["germandbls"] = 600,
+ ["Idieresis"] = 600,
+ ["eacute"] = 600,
+ ["abreve"] = 600,
+ ["uhungarumlaut"] = 600,
+ ["ecaron"] = 600,
+ ["Ydieresis"] = 600,
+ ["divide"] = 600,
+ ["Yacute"] = 600,
+ ["Acircumflex"] = 600,
+ ["aacute"] = 600,
+ ["Ucircumflex"] = 600,
+ ["yacute"] = 600,
+ ["scommaaccent"] = 600,
+ ["ecircumflex"] = 600,
+ ["Uring"] = 600,
+ ["Udieresis"] = 600,
+ ["aogonek"] = 600,
+ ["Uacute"] = 600,
+ ["uogonek"] = 600,
+ ["Edieresis"] = 600,
+ ["Dcroat"] = 600,
+ ["commaaccent"] = 600,
+ ["copyright"] = 600,
+ ["Emacron"] = 600,
+ ["ccaron"] = 600,
+ ["aring"] = 600,
+ ["Ncommaaccent"] = 600,
+ ["lacute"] = 600,
+ ["agrave"] = 600,
+ ["Tcommaaccent"] = 600,
+ ["Cacute"] = 600,
+ ["atilde"] = 600,
+ ["Edotaccent"] = 600,
+ ["scaron"] = 600,
+ ["scedilla"] = 600,
+ ["iacute"] = 600,
+ ["lozenge"] = 600,
+ ["Rcaron"] = 600,
+ ["Gcommaaccent"] = 600,
+ ["ucircumflex"] = 600,
+ ["acircumflex"] = 600,
+ ["Amacron"] = 600,
+ ["rcaron"] = 600,
+ ["ccedilla"] = 600,
+ ["Zdotaccent"] = 600,
+ ["Thorn"] = 600,
+ ["Omacron"] = 600,
+ ["Racute"] = 600,
+ ["Sacute"] = 600,
+ ["dcaron"] = 600,
+ ["Umacron"] = 600,
+ ["uring"] = 600,
+ ["threesuperior"] = 600,
+ ["Ograve"] = 600,
+ ["Agrave"] = 600,
+ ["Abreve"] = 600,
+ ["multiply"] = 600,
+ ["uacute"] = 600,
+ ["Tcaron"] = 600,
+ ["partialdiff"] = 600,
+ ["ydieresis"] = 600,
+ ["Nacute"] = 600,
+ ["icircumflex"] = 600,
+ ["Ecircumflex"] = 600,
+ ["adieresis"] = 600,
+ ["edieresis"] = 600,
+ ["cacute"] = 600,
+ ["nacute"] = 600,
+ ["umacron"] = 600,
+ ["Ncaron"] = 600,
+ ["Iacute"] = 600,
+ ["plusminus"] = 600,
+ ["brokenbar"] = 600,
+ ["registered"] = 600,
+ ["Gbreve"] = 600,
+ ["Idotaccent"] = 600,
+ ["summation"] = 600,
+ ["Egrave"] = 600,
+ ["racute"] = 600,
+ ["omacron"] = 600,
+ ["Zacute"] = 600,
+ ["Zcaron"] = 600,
+ ["greaterequal"] = 600,
+ ["Eth"] = 600,
+ ["Ccedilla"] = 600,
+ ["lcommaaccent"] = 600,
+ ["tcaron"] = 600,
+ ["eogonek"] = 600,
+ ["Uogonek"] = 600,
+ ["Aacute"] = 600,
+ ["Adieresis"] = 600,
+ ["egrave"] = 600,
+ ["zacute"] = 600,
+ ["iogonek"] = 600,
+ ["Oacute"] = 600,
+ ["oacute"] = 600,
+ ["amacron"] = 600,
+ ["sacute"] = 600,
+ ["idieresis"] = 600,
+ ["Ocircumflex"] = 600,
+ ["Ugrave"] = 600,
+ ["Delta"] = 600,
+ ["thorn"] = 600,
+ ["twosuperior"] = 600,
+ ["Odieresis"] = 600,
+ ["mu"] = 600,
+ ["igrave"] = 600,
+ ["ohungarumlaut"] = 600,
+ ["Eogonek"] = 600,
+ ["dcroat"] = 600,
+ ["threequarters"] = 600,
+ ["Scedilla"] = 600,
+ ["lcaron"] = 600,
+ ["Kcommaaccent"] = 600,
+ ["Lacute"] = 600,
+ ["trademark"] = 600,
+ ["edotaccent"] = 600,
+ ["Igrave"] = 600,
+ ["Imacron"] = 600,
+ ["Lcaron"] = 600,
+ ["onehalf"] = 600,
+ ["lessequal"] = 600,
+ ["ocircumflex"] = 600,
+ ["ntilde"] = 600,
+ ["Uhungarumlaut"] = 600,
+ ["Eacute"] = 600,
+ ["emacron"] = 600,
+ ["gbreve"] = 600,
+ ["onequarter"] = 600,
+ ["Scaron"] = 600,
+ ["Scommaaccent"] = 600,
+ ["Ohungarumlaut"] = 600,
+ ["degree"] = 600,
+ ["ograve"] = 600,
+ ["Ccaron"] = 600,
+ ["ugrave"] = 600,
+ ["radical"] = 600,
+ ["Dcaron"] = 600,
+ ["rcommaaccent"] = 600,
+ ["Ntilde"] = 600,
+ ["otilde"] = 600,
+ ["Rcommaaccent"] = 600,
+ ["Lcommaaccent"] = 600,
+ ["Atilde"] = 600,
+ ["Aogonek"] = 600,
+ ["Aring"] = 600,
+ ["Otilde"] = 600,
+ ["zdotaccent"] = 600,
+ ["Ecaron"] = 600,
+ ["Iogonek"] = 600,
+ ["kcommaaccent"] = 600,
+ ["minus"] = 600,
+ ["Icircumflex"] = 600,
+ ["ncaron"] = 600,
+ ["tcommaaccent"] = 600,
+ ["logicalnot"] = 600,
+ ["odieresis"] = 600,
+ ["udieresis"] = 600,
+ ["notequal"] = 600,
+ ["gcommaaccent"] = 600,
+ ["eth"] = 600,
+ ["zcaron"] = 600,
+ ["ncommaaccent"] = 600,
+ ["onesuperior"] = 600,
+ ["imacron"] = 600,
+ ["Euro"] = 600,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _courierBoldWidths = new Dictionary
+ {
+ ["space"] = 600,
+ ["exclam"] = 600,
+ ["quotedbl"] = 600,
+ ["numbersign"] = 600,
+ ["dollar"] = 600,
+ ["percent"] = 600,
+ ["ampersand"] = 600,
+ ["quoteright"] = 600,
+ ["parenleft"] = 600,
+ ["parenright"] = 600,
+ ["asterisk"] = 600,
+ ["plus"] = 600,
+ ["comma"] = 600,
+ ["hyphen"] = 600,
+ ["period"] = 600,
+ ["slash"] = 600,
+ ["zero"] = 600,
+ ["one"] = 600,
+ ["two"] = 600,
+ ["three"] = 600,
+ ["four"] = 600,
+ ["five"] = 600,
+ ["six"] = 600,
+ ["seven"] = 600,
+ ["eight"] = 600,
+ ["nine"] = 600,
+ ["colon"] = 600,
+ ["semicolon"] = 600,
+ ["less"] = 600,
+ ["equal"] = 600,
+ ["greater"] = 600,
+ ["question"] = 600,
+ ["at"] = 600,
+ ["A"] = 600,
+ ["B"] = 600,
+ ["C"] = 600,
+ ["D"] = 600,
+ ["E"] = 600,
+ ["F"] = 600,
+ ["G"] = 600,
+ ["H"] = 600,
+ ["I"] = 600,
+ ["J"] = 600,
+ ["K"] = 600,
+ ["L"] = 600,
+ ["M"] = 600,
+ ["N"] = 600,
+ ["O"] = 600,
+ ["P"] = 600,
+ ["Q"] = 600,
+ ["R"] = 600,
+ ["S"] = 600,
+ ["T"] = 600,
+ ["U"] = 600,
+ ["V"] = 600,
+ ["W"] = 600,
+ ["X"] = 600,
+ ["Y"] = 600,
+ ["Z"] = 600,
+ ["bracketleft"] = 600,
+ ["backslash"] = 600,
+ ["bracketright"] = 600,
+ ["asciicircum"] = 600,
+ ["underscore"] = 600,
+ ["quoteleft"] = 600,
+ ["a"] = 600,
+ ["b"] = 600,
+ ["c"] = 600,
+ ["d"] = 600,
+ ["e"] = 600,
+ ["f"] = 600,
+ ["g"] = 600,
+ ["h"] = 600,
+ ["i"] = 600,
+ ["j"] = 600,
+ ["k"] = 600,
+ ["l"] = 600,
+ ["m"] = 600,
+ ["n"] = 600,
+ ["o"] = 600,
+ ["p"] = 600,
+ ["q"] = 600,
+ ["r"] = 600,
+ ["s"] = 600,
+ ["t"] = 600,
+ ["u"] = 600,
+ ["v"] = 600,
+ ["w"] = 600,
+ ["x"] = 600,
+ ["y"] = 600,
+ ["z"] = 600,
+ ["braceleft"] = 600,
+ ["bar"] = 600,
+ ["braceright"] = 600,
+ ["asciitilde"] = 600,
+ ["exclamdown"] = 600,
+ ["cent"] = 600,
+ ["sterling"] = 600,
+ ["fraction"] = 600,
+ ["yen"] = 600,
+ ["florin"] = 600,
+ ["section"] = 600,
+ ["currency"] = 600,
+ ["quotesingle"] = 600,
+ ["quotedblleft"] = 600,
+ ["guillemotleft"] = 600,
+ ["guilsinglleft"] = 600,
+ ["guilsinglright"] = 600,
+ ["fi"] = 600,
+ ["fl"] = 600,
+ ["endash"] = 600,
+ ["dagger"] = 600,
+ ["daggerdbl"] = 600,
+ ["periodcentered"] = 600,
+ ["paragraph"] = 600,
+ ["bullet"] = 600,
+ ["quotesinglbase"] = 600,
+ ["quotedblbase"] = 600,
+ ["quotedblright"] = 600,
+ ["guillemotright"] = 600,
+ ["ellipsis"] = 600,
+ ["perthousand"] = 600,
+ ["questiondown"] = 600,
+ ["grave"] = 600,
+ ["acute"] = 600,
+ ["circumflex"] = 600,
+ ["tilde"] = 600,
+ ["macron"] = 600,
+ ["breve"] = 600,
+ ["dotaccent"] = 600,
+ ["dieresis"] = 600,
+ ["ring"] = 600,
+ ["cedilla"] = 600,
+ ["hungarumlaut"] = 600,
+ ["ogonek"] = 600,
+ ["caron"] = 600,
+ ["emdash"] = 600,
+ ["AE"] = 600,
+ ["ordfeminine"] = 600,
+ ["Lslash"] = 600,
+ ["Oslash"] = 600,
+ ["OE"] = 600,
+ ["ordmasculine"] = 600,
+ ["ae"] = 600,
+ ["dotlessi"] = 600,
+ ["lslash"] = 600,
+ ["oslash"] = 600,
+ ["oe"] = 600,
+ ["germandbls"] = 600,
+ ["Idieresis"] = 600,
+ ["eacute"] = 600,
+ ["abreve"] = 600,
+ ["uhungarumlaut"] = 600,
+ ["ecaron"] = 600,
+ ["Ydieresis"] = 600,
+ ["divide"] = 600,
+ ["Yacute"] = 600,
+ ["Acircumflex"] = 600,
+ ["aacute"] = 600,
+ ["Ucircumflex"] = 600,
+ ["yacute"] = 600,
+ ["scommaaccent"] = 600,
+ ["ecircumflex"] = 600,
+ ["Uring"] = 600,
+ ["Udieresis"] = 600,
+ ["aogonek"] = 600,
+ ["Uacute"] = 600,
+ ["uogonek"] = 600,
+ ["Edieresis"] = 600,
+ ["Dcroat"] = 600,
+ ["commaaccent"] = 600,
+ ["copyright"] = 600,
+ ["Emacron"] = 600,
+ ["ccaron"] = 600,
+ ["aring"] = 600,
+ ["Ncommaaccent"] = 600,
+ ["lacute"] = 600,
+ ["agrave"] = 600,
+ ["Tcommaaccent"] = 600,
+ ["Cacute"] = 600,
+ ["atilde"] = 600,
+ ["Edotaccent"] = 600,
+ ["scaron"] = 600,
+ ["scedilla"] = 600,
+ ["iacute"] = 600,
+ ["lozenge"] = 600,
+ ["Rcaron"] = 600,
+ ["Gcommaaccent"] = 600,
+ ["ucircumflex"] = 600,
+ ["acircumflex"] = 600,
+ ["Amacron"] = 600,
+ ["rcaron"] = 600,
+ ["ccedilla"] = 600,
+ ["Zdotaccent"] = 600,
+ ["Thorn"] = 600,
+ ["Omacron"] = 600,
+ ["Racute"] = 600,
+ ["Sacute"] = 600,
+ ["dcaron"] = 600,
+ ["Umacron"] = 600,
+ ["uring"] = 600,
+ ["threesuperior"] = 600,
+ ["Ograve"] = 600,
+ ["Agrave"] = 600,
+ ["Abreve"] = 600,
+ ["multiply"] = 600,
+ ["uacute"] = 600,
+ ["Tcaron"] = 600,
+ ["partialdiff"] = 600,
+ ["ydieresis"] = 600,
+ ["Nacute"] = 600,
+ ["icircumflex"] = 600,
+ ["Ecircumflex"] = 600,
+ ["adieresis"] = 600,
+ ["edieresis"] = 600,
+ ["cacute"] = 600,
+ ["nacute"] = 600,
+ ["umacron"] = 600,
+ ["Ncaron"] = 600,
+ ["Iacute"] = 600,
+ ["plusminus"] = 600,
+ ["brokenbar"] = 600,
+ ["registered"] = 600,
+ ["Gbreve"] = 600,
+ ["Idotaccent"] = 600,
+ ["summation"] = 600,
+ ["Egrave"] = 600,
+ ["racute"] = 600,
+ ["omacron"] = 600,
+ ["Zacute"] = 600,
+ ["Zcaron"] = 600,
+ ["greaterequal"] = 600,
+ ["Eth"] = 600,
+ ["Ccedilla"] = 600,
+ ["lcommaaccent"] = 600,
+ ["tcaron"] = 600,
+ ["eogonek"] = 600,
+ ["Uogonek"] = 600,
+ ["Aacute"] = 600,
+ ["Adieresis"] = 600,
+ ["egrave"] = 600,
+ ["zacute"] = 600,
+ ["iogonek"] = 600,
+ ["Oacute"] = 600,
+ ["oacute"] = 600,
+ ["amacron"] = 600,
+ ["sacute"] = 600,
+ ["idieresis"] = 600,
+ ["Ocircumflex"] = 600,
+ ["Ugrave"] = 600,
+ ["Delta"] = 600,
+ ["thorn"] = 600,
+ ["twosuperior"] = 600,
+ ["Odieresis"] = 600,
+ ["mu"] = 600,
+ ["igrave"] = 600,
+ ["ohungarumlaut"] = 600,
+ ["Eogonek"] = 600,
+ ["dcroat"] = 600,
+ ["threequarters"] = 600,
+ ["Scedilla"] = 600,
+ ["lcaron"] = 600,
+ ["Kcommaaccent"] = 600,
+ ["Lacute"] = 600,
+ ["trademark"] = 600,
+ ["edotaccent"] = 600,
+ ["Igrave"] = 600,
+ ["Imacron"] = 600,
+ ["Lcaron"] = 600,
+ ["onehalf"] = 600,
+ ["lessequal"] = 600,
+ ["ocircumflex"] = 600,
+ ["ntilde"] = 600,
+ ["Uhungarumlaut"] = 600,
+ ["Eacute"] = 600,
+ ["emacron"] = 600,
+ ["gbreve"] = 600,
+ ["onequarter"] = 600,
+ ["Scaron"] = 600,
+ ["Scommaaccent"] = 600,
+ ["Ohungarumlaut"] = 600,
+ ["degree"] = 600,
+ ["ograve"] = 600,
+ ["Ccaron"] = 600,
+ ["ugrave"] = 600,
+ ["radical"] = 600,
+ ["Dcaron"] = 600,
+ ["rcommaaccent"] = 600,
+ ["Ntilde"] = 600,
+ ["otilde"] = 600,
+ ["Rcommaaccent"] = 600,
+ ["Lcommaaccent"] = 600,
+ ["Atilde"] = 600,
+ ["Aogonek"] = 600,
+ ["Aring"] = 600,
+ ["Otilde"] = 600,
+ ["zdotaccent"] = 600,
+ ["Ecaron"] = 600,
+ ["Iogonek"] = 600,
+ ["kcommaaccent"] = 600,
+ ["minus"] = 600,
+ ["Icircumflex"] = 600,
+ ["ncaron"] = 600,
+ ["tcommaaccent"] = 600,
+ ["logicalnot"] = 600,
+ ["odieresis"] = 600,
+ ["udieresis"] = 600,
+ ["notequal"] = 600,
+ ["gcommaaccent"] = 600,
+ ["eth"] = 600,
+ ["zcaron"] = 600,
+ ["ncommaaccent"] = 600,
+ ["onesuperior"] = 600,
+ ["imacron"] = 600,
+ ["Euro"] = 600,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _courierObliqueWidths = new Dictionary
+ {
+ ["space"] = 600,
+ ["exclam"] = 600,
+ ["quotedbl"] = 600,
+ ["numbersign"] = 600,
+ ["dollar"] = 600,
+ ["percent"] = 600,
+ ["ampersand"] = 600,
+ ["quoteright"] = 600,
+ ["parenleft"] = 600,
+ ["parenright"] = 600,
+ ["asterisk"] = 600,
+ ["plus"] = 600,
+ ["comma"] = 600,
+ ["hyphen"] = 600,
+ ["period"] = 600,
+ ["slash"] = 600,
+ ["zero"] = 600,
+ ["one"] = 600,
+ ["two"] = 600,
+ ["three"] = 600,
+ ["four"] = 600,
+ ["five"] = 600,
+ ["six"] = 600,
+ ["seven"] = 600,
+ ["eight"] = 600,
+ ["nine"] = 600,
+ ["colon"] = 600,
+ ["semicolon"] = 600,
+ ["less"] = 600,
+ ["equal"] = 600,
+ ["greater"] = 600,
+ ["question"] = 600,
+ ["at"] = 600,
+ ["A"] = 600,
+ ["B"] = 600,
+ ["C"] = 600,
+ ["D"] = 600,
+ ["E"] = 600,
+ ["F"] = 600,
+ ["G"] = 600,
+ ["H"] = 600,
+ ["I"] = 600,
+ ["J"] = 600,
+ ["K"] = 600,
+ ["L"] = 600,
+ ["M"] = 600,
+ ["N"] = 600,
+ ["O"] = 600,
+ ["P"] = 600,
+ ["Q"] = 600,
+ ["R"] = 600,
+ ["S"] = 600,
+ ["T"] = 600,
+ ["U"] = 600,
+ ["V"] = 600,
+ ["W"] = 600,
+ ["X"] = 600,
+ ["Y"] = 600,
+ ["Z"] = 600,
+ ["bracketleft"] = 600,
+ ["backslash"] = 600,
+ ["bracketright"] = 600,
+ ["asciicircum"] = 600,
+ ["underscore"] = 600,
+ ["quoteleft"] = 600,
+ ["a"] = 600,
+ ["b"] = 600,
+ ["c"] = 600,
+ ["d"] = 600,
+ ["e"] = 600,
+ ["f"] = 600,
+ ["g"] = 600,
+ ["h"] = 600,
+ ["i"] = 600,
+ ["j"] = 600,
+ ["k"] = 600,
+ ["l"] = 600,
+ ["m"] = 600,
+ ["n"] = 600,
+ ["o"] = 600,
+ ["p"] = 600,
+ ["q"] = 600,
+ ["r"] = 600,
+ ["s"] = 600,
+ ["t"] = 600,
+ ["u"] = 600,
+ ["v"] = 600,
+ ["w"] = 600,
+ ["x"] = 600,
+ ["y"] = 600,
+ ["z"] = 600,
+ ["braceleft"] = 600,
+ ["bar"] = 600,
+ ["braceright"] = 600,
+ ["asciitilde"] = 600,
+ ["exclamdown"] = 600,
+ ["cent"] = 600,
+ ["sterling"] = 600,
+ ["fraction"] = 600,
+ ["yen"] = 600,
+ ["florin"] = 600,
+ ["section"] = 600,
+ ["currency"] = 600,
+ ["quotesingle"] = 600,
+ ["quotedblleft"] = 600,
+ ["guillemotleft"] = 600,
+ ["guilsinglleft"] = 600,
+ ["guilsinglright"] = 600,
+ ["fi"] = 600,
+ ["fl"] = 600,
+ ["endash"] = 600,
+ ["dagger"] = 600,
+ ["daggerdbl"] = 600,
+ ["periodcentered"] = 600,
+ ["paragraph"] = 600,
+ ["bullet"] = 600,
+ ["quotesinglbase"] = 600,
+ ["quotedblbase"] = 600,
+ ["quotedblright"] = 600,
+ ["guillemotright"] = 600,
+ ["ellipsis"] = 600,
+ ["perthousand"] = 600,
+ ["questiondown"] = 600,
+ ["grave"] = 600,
+ ["acute"] = 600,
+ ["circumflex"] = 600,
+ ["tilde"] = 600,
+ ["macron"] = 600,
+ ["breve"] = 600,
+ ["dotaccent"] = 600,
+ ["dieresis"] = 600,
+ ["ring"] = 600,
+ ["cedilla"] = 600,
+ ["hungarumlaut"] = 600,
+ ["ogonek"] = 600,
+ ["caron"] = 600,
+ ["emdash"] = 600,
+ ["AE"] = 600,
+ ["ordfeminine"] = 600,
+ ["Lslash"] = 600,
+ ["Oslash"] = 600,
+ ["OE"] = 600,
+ ["ordmasculine"] = 600,
+ ["ae"] = 600,
+ ["dotlessi"] = 600,
+ ["lslash"] = 600,
+ ["oslash"] = 600,
+ ["oe"] = 600,
+ ["germandbls"] = 600,
+ ["Idieresis"] = 600,
+ ["eacute"] = 600,
+ ["abreve"] = 600,
+ ["uhungarumlaut"] = 600,
+ ["ecaron"] = 600,
+ ["Ydieresis"] = 600,
+ ["divide"] = 600,
+ ["Yacute"] = 600,
+ ["Acircumflex"] = 600,
+ ["aacute"] = 600,
+ ["Ucircumflex"] = 600,
+ ["yacute"] = 600,
+ ["scommaaccent"] = 600,
+ ["ecircumflex"] = 600,
+ ["Uring"] = 600,
+ ["Udieresis"] = 600,
+ ["aogonek"] = 600,
+ ["Uacute"] = 600,
+ ["uogonek"] = 600,
+ ["Edieresis"] = 600,
+ ["Dcroat"] = 600,
+ ["commaaccent"] = 600,
+ ["copyright"] = 600,
+ ["Emacron"] = 600,
+ ["ccaron"] = 600,
+ ["aring"] = 600,
+ ["Ncommaaccent"] = 600,
+ ["lacute"] = 600,
+ ["agrave"] = 600,
+ ["Tcommaaccent"] = 600,
+ ["Cacute"] = 600,
+ ["atilde"] = 600,
+ ["Edotaccent"] = 600,
+ ["scaron"] = 600,
+ ["scedilla"] = 600,
+ ["iacute"] = 600,
+ ["lozenge"] = 600,
+ ["Rcaron"] = 600,
+ ["Gcommaaccent"] = 600,
+ ["ucircumflex"] = 600,
+ ["acircumflex"] = 600,
+ ["Amacron"] = 600,
+ ["rcaron"] = 600,
+ ["ccedilla"] = 600,
+ ["Zdotaccent"] = 600,
+ ["Thorn"] = 600,
+ ["Omacron"] = 600,
+ ["Racute"] = 600,
+ ["Sacute"] = 600,
+ ["dcaron"] = 600,
+ ["Umacron"] = 600,
+ ["uring"] = 600,
+ ["threesuperior"] = 600,
+ ["Ograve"] = 600,
+ ["Agrave"] = 600,
+ ["Abreve"] = 600,
+ ["multiply"] = 600,
+ ["uacute"] = 600,
+ ["Tcaron"] = 600,
+ ["partialdiff"] = 600,
+ ["ydieresis"] = 600,
+ ["Nacute"] = 600,
+ ["icircumflex"] = 600,
+ ["Ecircumflex"] = 600,
+ ["adieresis"] = 600,
+ ["edieresis"] = 600,
+ ["cacute"] = 600,
+ ["nacute"] = 600,
+ ["umacron"] = 600,
+ ["Ncaron"] = 600,
+ ["Iacute"] = 600,
+ ["plusminus"] = 600,
+ ["brokenbar"] = 600,
+ ["registered"] = 600,
+ ["Gbreve"] = 600,
+ ["Idotaccent"] = 600,
+ ["summation"] = 600,
+ ["Egrave"] = 600,
+ ["racute"] = 600,
+ ["omacron"] = 600,
+ ["Zacute"] = 600,
+ ["Zcaron"] = 600,
+ ["greaterequal"] = 600,
+ ["Eth"] = 600,
+ ["Ccedilla"] = 600,
+ ["lcommaaccent"] = 600,
+ ["tcaron"] = 600,
+ ["eogonek"] = 600,
+ ["Uogonek"] = 600,
+ ["Aacute"] = 600,
+ ["Adieresis"] = 600,
+ ["egrave"] = 600,
+ ["zacute"] = 600,
+ ["iogonek"] = 600,
+ ["Oacute"] = 600,
+ ["oacute"] = 600,
+ ["amacron"] = 600,
+ ["sacute"] = 600,
+ ["idieresis"] = 600,
+ ["Ocircumflex"] = 600,
+ ["Ugrave"] = 600,
+ ["Delta"] = 600,
+ ["thorn"] = 600,
+ ["twosuperior"] = 600,
+ ["Odieresis"] = 600,
+ ["mu"] = 600,
+ ["igrave"] = 600,
+ ["ohungarumlaut"] = 600,
+ ["Eogonek"] = 600,
+ ["dcroat"] = 600,
+ ["threequarters"] = 600,
+ ["Scedilla"] = 600,
+ ["lcaron"] = 600,
+ ["Kcommaaccent"] = 600,
+ ["Lacute"] = 600,
+ ["trademark"] = 600,
+ ["edotaccent"] = 600,
+ ["Igrave"] = 600,
+ ["Imacron"] = 600,
+ ["Lcaron"] = 600,
+ ["onehalf"] = 600,
+ ["lessequal"] = 600,
+ ["ocircumflex"] = 600,
+ ["ntilde"] = 600,
+ ["Uhungarumlaut"] = 600,
+ ["Eacute"] = 600,
+ ["emacron"] = 600,
+ ["gbreve"] = 600,
+ ["onequarter"] = 600,
+ ["Scaron"] = 600,
+ ["Scommaaccent"] = 600,
+ ["Ohungarumlaut"] = 600,
+ ["degree"] = 600,
+ ["ograve"] = 600,
+ ["Ccaron"] = 600,
+ ["ugrave"] = 600,
+ ["radical"] = 600,
+ ["Dcaron"] = 600,
+ ["rcommaaccent"] = 600,
+ ["Ntilde"] = 600,
+ ["otilde"] = 600,
+ ["Rcommaaccent"] = 600,
+ ["Lcommaaccent"] = 600,
+ ["Atilde"] = 600,
+ ["Aogonek"] = 600,
+ ["Aring"] = 600,
+ ["Otilde"] = 600,
+ ["zdotaccent"] = 600,
+ ["Ecaron"] = 600,
+ ["Iogonek"] = 600,
+ ["kcommaaccent"] = 600,
+ ["minus"] = 600,
+ ["Icircumflex"] = 600,
+ ["ncaron"] = 600,
+ ["tcommaaccent"] = 600,
+ ["logicalnot"] = 600,
+ ["odieresis"] = 600,
+ ["udieresis"] = 600,
+ ["notequal"] = 600,
+ ["gcommaaccent"] = 600,
+ ["eth"] = 600,
+ ["zcaron"] = 600,
+ ["ncommaaccent"] = 600,
+ ["onesuperior"] = 600,
+ ["imacron"] = 600,
+ ["Euro"] = 600,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _courierBoldObliqueWidths = new Dictionary
+ {
+ ["space"] = 600,
+ ["exclam"] = 600,
+ ["quotedbl"] = 600,
+ ["numbersign"] = 600,
+ ["dollar"] = 600,
+ ["percent"] = 600,
+ ["ampersand"] = 600,
+ ["quoteright"] = 600,
+ ["parenleft"] = 600,
+ ["parenright"] = 600,
+ ["asterisk"] = 600,
+ ["plus"] = 600,
+ ["comma"] = 600,
+ ["hyphen"] = 600,
+ ["period"] = 600,
+ ["slash"] = 600,
+ ["zero"] = 600,
+ ["one"] = 600,
+ ["two"] = 600,
+ ["three"] = 600,
+ ["four"] = 600,
+ ["five"] = 600,
+ ["six"] = 600,
+ ["seven"] = 600,
+ ["eight"] = 600,
+ ["nine"] = 600,
+ ["colon"] = 600,
+ ["semicolon"] = 600,
+ ["less"] = 600,
+ ["equal"] = 600,
+ ["greater"] = 600,
+ ["question"] = 600,
+ ["at"] = 600,
+ ["A"] = 600,
+ ["B"] = 600,
+ ["C"] = 600,
+ ["D"] = 600,
+ ["E"] = 600,
+ ["F"] = 600,
+ ["G"] = 600,
+ ["H"] = 600,
+ ["I"] = 600,
+ ["J"] = 600,
+ ["K"] = 600,
+ ["L"] = 600,
+ ["M"] = 600,
+ ["N"] = 600,
+ ["O"] = 600,
+ ["P"] = 600,
+ ["Q"] = 600,
+ ["R"] = 600,
+ ["S"] = 600,
+ ["T"] = 600,
+ ["U"] = 600,
+ ["V"] = 600,
+ ["W"] = 600,
+ ["X"] = 600,
+ ["Y"] = 600,
+ ["Z"] = 600,
+ ["bracketleft"] = 600,
+ ["backslash"] = 600,
+ ["bracketright"] = 600,
+ ["asciicircum"] = 600,
+ ["underscore"] = 600,
+ ["quoteleft"] = 600,
+ ["a"] = 600,
+ ["b"] = 600,
+ ["c"] = 600,
+ ["d"] = 600,
+ ["e"] = 600,
+ ["f"] = 600,
+ ["g"] = 600,
+ ["h"] = 600,
+ ["i"] = 600,
+ ["j"] = 600,
+ ["k"] = 600,
+ ["l"] = 600,
+ ["m"] = 600,
+ ["n"] = 600,
+ ["o"] = 600,
+ ["p"] = 600,
+ ["q"] = 600,
+ ["r"] = 600,
+ ["s"] = 600,
+ ["t"] = 600,
+ ["u"] = 600,
+ ["v"] = 600,
+ ["w"] = 600,
+ ["x"] = 600,
+ ["y"] = 600,
+ ["z"] = 600,
+ ["braceleft"] = 600,
+ ["bar"] = 600,
+ ["braceright"] = 600,
+ ["asciitilde"] = 600,
+ ["exclamdown"] = 600,
+ ["cent"] = 600,
+ ["sterling"] = 600,
+ ["fraction"] = 600,
+ ["yen"] = 600,
+ ["florin"] = 600,
+ ["section"] = 600,
+ ["currency"] = 600,
+ ["quotesingle"] = 600,
+ ["quotedblleft"] = 600,
+ ["guillemotleft"] = 600,
+ ["guilsinglleft"] = 600,
+ ["guilsinglright"] = 600,
+ ["fi"] = 600,
+ ["fl"] = 600,
+ ["endash"] = 600,
+ ["dagger"] = 600,
+ ["daggerdbl"] = 600,
+ ["periodcentered"] = 600,
+ ["paragraph"] = 600,
+ ["bullet"] = 600,
+ ["quotesinglbase"] = 600,
+ ["quotedblbase"] = 600,
+ ["quotedblright"] = 600,
+ ["guillemotright"] = 600,
+ ["ellipsis"] = 600,
+ ["perthousand"] = 600,
+ ["questiondown"] = 600,
+ ["grave"] = 600,
+ ["acute"] = 600,
+ ["circumflex"] = 600,
+ ["tilde"] = 600,
+ ["macron"] = 600,
+ ["breve"] = 600,
+ ["dotaccent"] = 600,
+ ["dieresis"] = 600,
+ ["ring"] = 600,
+ ["cedilla"] = 600,
+ ["hungarumlaut"] = 600,
+ ["ogonek"] = 600,
+ ["caron"] = 600,
+ ["emdash"] = 600,
+ ["AE"] = 600,
+ ["ordfeminine"] = 600,
+ ["Lslash"] = 600,
+ ["Oslash"] = 600,
+ ["OE"] = 600,
+ ["ordmasculine"] = 600,
+ ["ae"] = 600,
+ ["dotlessi"] = 600,
+ ["lslash"] = 600,
+ ["oslash"] = 600,
+ ["oe"] = 600,
+ ["germandbls"] = 600,
+ ["Idieresis"] = 600,
+ ["eacute"] = 600,
+ ["abreve"] = 600,
+ ["uhungarumlaut"] = 600,
+ ["ecaron"] = 600,
+ ["Ydieresis"] = 600,
+ ["divide"] = 600,
+ ["Yacute"] = 600,
+ ["Acircumflex"] = 600,
+ ["aacute"] = 600,
+ ["Ucircumflex"] = 600,
+ ["yacute"] = 600,
+ ["scommaaccent"] = 600,
+ ["ecircumflex"] = 600,
+ ["Uring"] = 600,
+ ["Udieresis"] = 600,
+ ["aogonek"] = 600,
+ ["Uacute"] = 600,
+ ["uogonek"] = 600,
+ ["Edieresis"] = 600,
+ ["Dcroat"] = 600,
+ ["commaaccent"] = 600,
+ ["copyright"] = 600,
+ ["Emacron"] = 600,
+ ["ccaron"] = 600,
+ ["aring"] = 600,
+ ["Ncommaaccent"] = 600,
+ ["lacute"] = 600,
+ ["agrave"] = 600,
+ ["Tcommaaccent"] = 600,
+ ["Cacute"] = 600,
+ ["atilde"] = 600,
+ ["Edotaccent"] = 600,
+ ["scaron"] = 600,
+ ["scedilla"] = 600,
+ ["iacute"] = 600,
+ ["lozenge"] = 600,
+ ["Rcaron"] = 600,
+ ["Gcommaaccent"] = 600,
+ ["ucircumflex"] = 600,
+ ["acircumflex"] = 600,
+ ["Amacron"] = 600,
+ ["rcaron"] = 600,
+ ["ccedilla"] = 600,
+ ["Zdotaccent"] = 600,
+ ["Thorn"] = 600,
+ ["Omacron"] = 600,
+ ["Racute"] = 600,
+ ["Sacute"] = 600,
+ ["dcaron"] = 600,
+ ["Umacron"] = 600,
+ ["uring"] = 600,
+ ["threesuperior"] = 600,
+ ["Ograve"] = 600,
+ ["Agrave"] = 600,
+ ["Abreve"] = 600,
+ ["multiply"] = 600,
+ ["uacute"] = 600,
+ ["Tcaron"] = 600,
+ ["partialdiff"] = 600,
+ ["ydieresis"] = 600,
+ ["Nacute"] = 600,
+ ["icircumflex"] = 600,
+ ["Ecircumflex"] = 600,
+ ["adieresis"] = 600,
+ ["edieresis"] = 600,
+ ["cacute"] = 600,
+ ["nacute"] = 600,
+ ["umacron"] = 600,
+ ["Ncaron"] = 600,
+ ["Iacute"] = 600,
+ ["plusminus"] = 600,
+ ["brokenbar"] = 600,
+ ["registered"] = 600,
+ ["Gbreve"] = 600,
+ ["Idotaccent"] = 600,
+ ["summation"] = 600,
+ ["Egrave"] = 600,
+ ["racute"] = 600,
+ ["omacron"] = 600,
+ ["Zacute"] = 600,
+ ["Zcaron"] = 600,
+ ["greaterequal"] = 600,
+ ["Eth"] = 600,
+ ["Ccedilla"] = 600,
+ ["lcommaaccent"] = 600,
+ ["tcaron"] = 600,
+ ["eogonek"] = 600,
+ ["Uogonek"] = 600,
+ ["Aacute"] = 600,
+ ["Adieresis"] = 600,
+ ["egrave"] = 600,
+ ["zacute"] = 600,
+ ["iogonek"] = 600,
+ ["Oacute"] = 600,
+ ["oacute"] = 600,
+ ["amacron"] = 600,
+ ["sacute"] = 600,
+ ["idieresis"] = 600,
+ ["Ocircumflex"] = 600,
+ ["Ugrave"] = 600,
+ ["Delta"] = 600,
+ ["thorn"] = 600,
+ ["twosuperior"] = 600,
+ ["Odieresis"] = 600,
+ ["mu"] = 600,
+ ["igrave"] = 600,
+ ["ohungarumlaut"] = 600,
+ ["Eogonek"] = 600,
+ ["dcroat"] = 600,
+ ["threequarters"] = 600,
+ ["Scedilla"] = 600,
+ ["lcaron"] = 600,
+ ["Kcommaaccent"] = 600,
+ ["Lacute"] = 600,
+ ["trademark"] = 600,
+ ["edotaccent"] = 600,
+ ["Igrave"] = 600,
+ ["Imacron"] = 600,
+ ["Lcaron"] = 600,
+ ["onehalf"] = 600,
+ ["lessequal"] = 600,
+ ["ocircumflex"] = 600,
+ ["ntilde"] = 600,
+ ["Uhungarumlaut"] = 600,
+ ["Eacute"] = 600,
+ ["emacron"] = 600,
+ ["gbreve"] = 600,
+ ["onequarter"] = 600,
+ ["Scaron"] = 600,
+ ["Scommaaccent"] = 600,
+ ["Ohungarumlaut"] = 600,
+ ["degree"] = 600,
+ ["ograve"] = 600,
+ ["Ccaron"] = 600,
+ ["ugrave"] = 600,
+ ["radical"] = 600,
+ ["Dcaron"] = 600,
+ ["rcommaaccent"] = 600,
+ ["Ntilde"] = 600,
+ ["otilde"] = 600,
+ ["Rcommaaccent"] = 600,
+ ["Lcommaaccent"] = 600,
+ ["Atilde"] = 600,
+ ["Aogonek"] = 600,
+ ["Aring"] = 600,
+ ["Otilde"] = 600,
+ ["zdotaccent"] = 600,
+ ["Ecaron"] = 600,
+ ["Iogonek"] = 600,
+ ["kcommaaccent"] = 600,
+ ["minus"] = 600,
+ ["Icircumflex"] = 600,
+ ["ncaron"] = 600,
+ ["tcommaaccent"] = 600,
+ ["logicalnot"] = 600,
+ ["odieresis"] = 600,
+ ["udieresis"] = 600,
+ ["notequal"] = 600,
+ ["gcommaaccent"] = 600,
+ ["eth"] = 600,
+ ["zcaron"] = 600,
+ ["ncommaaccent"] = 600,
+ ["onesuperior"] = 600,
+ ["imacron"] = 600,
+ ["Euro"] = 600,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _symbolWidths = new Dictionary
+ {
+ ["space"] = 250,
+ ["exclam"] = 333,
+ ["universal"] = 713,
+ ["numbersign"] = 500,
+ ["existential"] = 549,
+ ["percent"] = 833,
+ ["ampersand"] = 778,
+ ["suchthat"] = 439,
+ ["parenleft"] = 333,
+ ["parenright"] = 333,
+ ["asteriskmath"] = 500,
+ ["plus"] = 549,
+ ["comma"] = 250,
+ ["minus"] = 549,
+ ["period"] = 250,
+ ["slash"] = 278,
+ ["zero"] = 500,
+ ["one"] = 500,
+ ["two"] = 500,
+ ["three"] = 500,
+ ["four"] = 500,
+ ["five"] = 500,
+ ["six"] = 500,
+ ["seven"] = 500,
+ ["eight"] = 500,
+ ["nine"] = 500,
+ ["colon"] = 278,
+ ["semicolon"] = 278,
+ ["less"] = 549,
+ ["equal"] = 549,
+ ["greater"] = 549,
+ ["question"] = 444,
+ ["congruent"] = 549,
+ ["Alpha"] = 722,
+ ["Beta"] = 667,
+ ["Chi"] = 722,
+ ["Delta"] = 612,
+ ["Epsilon"] = 611,
+ ["Phi"] = 763,
+ ["Gamma"] = 603,
+ ["Eta"] = 722,
+ ["Iota"] = 333,
+ ["theta1"] = 631,
+ ["Kappa"] = 722,
+ ["Lambda"] = 686,
+ ["Mu"] = 889,
+ ["Nu"] = 722,
+ ["Omicron"] = 722,
+ ["Pi"] = 768,
+ ["Theta"] = 741,
+ ["Rho"] = 556,
+ ["Sigma"] = 592,
+ ["Tau"] = 611,
+ ["Upsilon"] = 690,
+ ["sigma1"] = 439,
+ ["Omega"] = 768,
+ ["Xi"] = 645,
+ ["Psi"] = 795,
+ ["Zeta"] = 611,
+ ["bracketleft"] = 333,
+ ["therefore"] = 863,
+ ["bracketright"] = 333,
+ ["perpendicular"] = 658,
+ ["underscore"] = 500,
+ ["radicalex"] = 500,
+ ["alpha"] = 631,
+ ["beta"] = 549,
+ ["chi"] = 549,
+ ["delta"] = 494,
+ ["epsilon"] = 439,
+ ["phi"] = 521,
+ ["gamma"] = 411,
+ ["eta"] = 603,
+ ["iota"] = 329,
+ ["phi1"] = 603,
+ ["kappa"] = 549,
+ ["lambda"] = 549,
+ ["mu"] = 576,
+ ["nu"] = 521,
+ ["omicron"] = 549,
+ ["pi"] = 549,
+ ["theta"] = 521,
+ ["rho"] = 549,
+ ["sigma"] = 603,
+ ["tau"] = 439,
+ ["upsilon"] = 576,
+ ["omega1"] = 713,
+ ["omega"] = 686,
+ ["xi"] = 493,
+ ["psi"] = 686,
+ ["zeta"] = 494,
+ ["braceleft"] = 480,
+ ["bar"] = 200,
+ ["braceright"] = 480,
+ ["similar"] = 549,
+ ["Euro"] = 750,
+ ["Upsilon1"] = 620,
+ ["minute"] = 247,
+ ["lessequal"] = 549,
+ ["fraction"] = 167,
+ ["infinity"] = 713,
+ ["florin"] = 500,
+ ["club"] = 753,
+ ["diamond"] = 753,
+ ["heart"] = 753,
+ ["spade"] = 753,
+ ["arrowboth"] = 1042,
+ ["arrowleft"] = 987,
+ ["arrowup"] = 603,
+ ["arrowright"] = 987,
+ ["arrowdown"] = 603,
+ ["degree"] = 400,
+ ["plusminus"] = 549,
+ ["second"] = 411,
+ ["greaterequal"] = 549,
+ ["multiply"] = 549,
+ ["proportional"] = 713,
+ ["partialdiff"] = 494,
+ ["bullet"] = 460,
+ ["divide"] = 549,
+ ["notequal"] = 549,
+ ["equivalence"] = 549,
+ ["approxequal"] = 549,
+ ["ellipsis"] = 1000,
+ ["arrowvertex"] = 603,
+ ["arrowhorizex"] = 1000,
+ ["carriagereturn"] = 658,
+ ["aleph"] = 823,
+ ["Ifraktur"] = 686,
+ ["Rfraktur"] = 795,
+ ["weierstrass"] = 987,
+ ["circlemultiply"] = 768,
+ ["circleplus"] = 768,
+ ["emptyset"] = 823,
+ ["intersection"] = 768,
+ ["union"] = 768,
+ ["propersuperset"] = 713,
+ ["reflexsuperset"] = 713,
+ ["notsubset"] = 713,
+ ["propersubset"] = 713,
+ ["reflexsubset"] = 713,
+ ["element"] = 713,
+ ["notelement"] = 713,
+ ["angle"] = 768,
+ ["gradient"] = 713,
+ ["registerserif"] = 790,
+ ["copyrightserif"] = 790,
+ ["trademarkserif"] = 890,
+ ["product"] = 823,
+ ["radical"] = 549,
+ ["dotmath"] = 250,
+ ["logicalnot"] = 713,
+ ["logicaland"] = 603,
+ ["logicalor"] = 603,
+ ["arrowdblboth"] = 1042,
+ ["arrowdblleft"] = 987,
+ ["arrowdblup"] = 603,
+ ["arrowdblright"] = 987,
+ ["arrowdbldown"] = 603,
+ ["lozenge"] = 494,
+ ["angleleft"] = 329,
+ ["registersans"] = 790,
+ ["copyrightsans"] = 790,
+ ["trademarksans"] = 786,
+ ["summation"] = 713,
+ ["parenlefttp"] = 384,
+ ["parenleftex"] = 384,
+ ["parenleftbt"] = 384,
+ ["bracketlefttp"] = 384,
+ ["bracketleftex"] = 384,
+ ["bracketleftbt"] = 384,
+ ["bracelefttp"] = 494,
+ ["braceleftmid"] = 494,
+ ["braceleftbt"] = 494,
+ ["braceex"] = 494,
+ ["angleright"] = 329,
+ ["integral"] = 274,
+ ["integraltp"] = 686,
+ ["integralex"] = 686,
+ ["integralbt"] = 686,
+ ["parenrighttp"] = 384,
+ ["parenrightex"] = 384,
+ ["parenrightbt"] = 384,
+ ["bracketrighttp"] = 384,
+ ["bracketrightex"] = 384,
+ ["bracketrightbt"] = 384,
+ ["bracerighttp"] = 494,
+ ["bracerightmid"] = 494,
+ ["bracerightbt"] = 494,
+ ["apple"] = 790,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary _zapfDingbatsWidths = new Dictionary
+ {
+ ["space"] = 278,
+ ["a1"] = 974,
+ ["a2"] = 961,
+ ["a202"] = 974,
+ ["a3"] = 980,
+ ["a4"] = 719,
+ ["a5"] = 789,
+ ["a119"] = 790,
+ ["a118"] = 791,
+ ["a117"] = 690,
+ ["a11"] = 960,
+ ["a12"] = 939,
+ ["a13"] = 549,
+ ["a14"] = 855,
+ ["a15"] = 911,
+ ["a16"] = 933,
+ ["a105"] = 911,
+ ["a17"] = 945,
+ ["a18"] = 974,
+ ["a19"] = 755,
+ ["a20"] = 846,
+ ["a21"] = 762,
+ ["a22"] = 761,
+ ["a23"] = 571,
+ ["a24"] = 677,
+ ["a25"] = 763,
+ ["a26"] = 760,
+ ["a27"] = 759,
+ ["a28"] = 754,
+ ["a6"] = 494,
+ ["a7"] = 552,
+ ["a8"] = 537,
+ ["a9"] = 577,
+ ["a10"] = 692,
+ ["a29"] = 786,
+ ["a30"] = 788,
+ ["a31"] = 788,
+ ["a32"] = 790,
+ ["a33"] = 793,
+ ["a34"] = 794,
+ ["a35"] = 816,
+ ["a36"] = 823,
+ ["a37"] = 789,
+ ["a38"] = 841,
+ ["a39"] = 823,
+ ["a40"] = 833,
+ ["a41"] = 816,
+ ["a42"] = 831,
+ ["a43"] = 923,
+ ["a44"] = 744,
+ ["a45"] = 723,
+ ["a46"] = 749,
+ ["a47"] = 790,
+ ["a48"] = 792,
+ ["a49"] = 695,
+ ["a50"] = 776,
+ ["a51"] = 768,
+ ["a52"] = 792,
+ ["a53"] = 759,
+ ["a54"] = 707,
+ ["a55"] = 708,
+ ["a56"] = 682,
+ ["a57"] = 701,
+ ["a58"] = 826,
+ ["a59"] = 815,
+ ["a60"] = 789,
+ ["a61"] = 789,
+ ["a62"] = 707,
+ ["a63"] = 687,
+ ["a64"] = 696,
+ ["a65"] = 689,
+ ["a66"] = 786,
+ ["a67"] = 787,
+ ["a68"] = 713,
+ ["a69"] = 791,
+ ["a70"] = 785,
+ ["a71"] = 791,
+ ["a72"] = 873,
+ ["a73"] = 761,
+ ["a74"] = 762,
+ ["a203"] = 762,
+ ["a75"] = 759,
+ ["a204"] = 759,
+ ["a76"] = 892,
+ ["a77"] = 892,
+ ["a78"] = 788,
+ ["a79"] = 784,
+ ["a81"] = 438,
+ ["a82"] = 138,
+ ["a83"] = 277,
+ ["a84"] = 415,
+ ["a97"] = 392,
+ ["a98"] = 392,
+ ["a99"] = 668,
+ ["a100"] = 668,
+ ["a89"] = 390,
+ ["a90"] = 390,
+ ["a93"] = 317,
+ ["a94"] = 317,
+ ["a91"] = 276,
+ ["a92"] = 276,
+ ["a205"] = 509,
+ ["a85"] = 509,
+ ["a206"] = 410,
+ ["a86"] = 410,
+ ["a87"] = 234,
+ ["a88"] = 234,
+ ["a95"] = 334,
+ ["a96"] = 334,
+ ["a101"] = 732,
+ ["a102"] = 544,
+ ["a103"] = 544,
+ ["a104"] = 910,
+ ["a106"] = 667,
+ ["a107"] = 760,
+ ["a108"] = 760,
+ ["a112"] = 776,
+ ["a111"] = 595,
+ ["a110"] = 694,
+ ["a109"] = 626,
+ ["a120"] = 788,
+ ["a121"] = 788,
+ ["a122"] = 788,
+ ["a123"] = 788,
+ ["a124"] = 788,
+ ["a125"] = 788,
+ ["a126"] = 788,
+ ["a127"] = 788,
+ ["a128"] = 788,
+ ["a129"] = 788,
+ ["a130"] = 788,
+ ["a131"] = 788,
+ ["a132"] = 788,
+ ["a133"] = 788,
+ ["a134"] = 788,
+ ["a135"] = 788,
+ ["a136"] = 788,
+ ["a137"] = 788,
+ ["a138"] = 788,
+ ["a139"] = 788,
+ ["a140"] = 788,
+ ["a141"] = 788,
+ ["a142"] = 788,
+ ["a143"] = 788,
+ ["a144"] = 788,
+ ["a145"] = 788,
+ ["a146"] = 788,
+ ["a147"] = 788,
+ ["a148"] = 788,
+ ["a149"] = 788,
+ ["a150"] = 788,
+ ["a151"] = 788,
+ ["a152"] = 788,
+ ["a153"] = 788,
+ ["a154"] = 788,
+ ["a155"] = 788,
+ ["a156"] = 788,
+ ["a157"] = 788,
+ ["a158"] = 788,
+ ["a159"] = 788,
+ ["a160"] = 894,
+ ["a161"] = 838,
+ ["a163"] = 1016,
+ ["a164"] = 458,
+ ["a196"] = 748,
+ ["a165"] = 924,
+ ["a192"] = 748,
+ ["a166"] = 918,
+ ["a167"] = 927,
+ ["a168"] = 928,
+ ["a169"] = 928,
+ ["a170"] = 834,
+ ["a171"] = 873,
+ ["a172"] = 828,
+ ["a173"] = 924,
+ ["a162"] = 924,
+ ["a174"] = 917,
+ ["a175"] = 930,
+ ["a176"] = 931,
+ ["a177"] = 463,
+ ["a178"] = 883,
+ ["a179"] = 836,
+ ["a193"] = 836,
+ ["a180"] = 867,
+ ["a199"] = 867,
+ ["a181"] = 696,
+ ["a200"] = 696,
+ ["a182"] = 874,
+ ["a201"] = 874,
+ ["a183"] = 760,
+ ["a184"] = 946,
+ ["a197"] = 771,
+ ["a185"] = 865,
+ ["a194"] = 771,
+ ["a198"] = 888,
+ ["a186"] = 967,
+ ["a195"] = 888,
+ ["a187"] = 831,
+ ["a188"] = 873,
+ ["a189"] = 927,
+ ["a190"] = 970,
+ ["a191"] = 918,
+ }.ToFrozenDictionary();
+
+ private static readonly FrozenDictionary> _textFontWidths =
+ new Dictionary>
+ {
+ ["Helvetica"] = _helveticaWidths,
+ ["Helvetica-Bold"] = _helveticaBoldWidths,
+ ["Helvetica-Oblique"] = _helveticaObliqueWidths,
+ ["Helvetica-BoldOblique"] = _helveticaBoldObliqueWidths,
+ ["Times-Roman"] = _timesRomanWidths,
+ ["Times-Bold"] = _timesBoldWidths,
+ ["Times-Italic"] = _timesItalicWidths,
+ ["Times-BoldItalic"] = _timesBoldItalicWidths,
+ ["Courier"] = _courierWidths,
+ ["Courier-Bold"] = _courierBoldWidths,
+ ["Courier-Oblique"] = _courierObliqueWidths,
+ ["Courier-BoldOblique"] = _courierBoldObliqueWidths,
+ }.ToFrozenDictionary();
+
+ private static string?[] BuildEncoding_symbol()
+ {
+ var t = new string?[256];
+ t[0x20] = "space";
+ t[0x21] = "exclam";
+ t[0x22] = "universal";
+ t[0x23] = "numbersign";
+ t[0x24] = "existential";
+ t[0x25] = "percent";
+ t[0x26] = "ampersand";
+ t[0x27] = "suchthat";
+ t[0x28] = "parenleft";
+ t[0x29] = "parenright";
+ t[0x2A] = "asteriskmath";
+ t[0x2B] = "plus";
+ t[0x2C] = "comma";
+ t[0x2D] = "minus";
+ t[0x2E] = "period";
+ t[0x2F] = "slash";
+ t[0x30] = "zero";
+ t[0x31] = "one";
+ t[0x32] = "two";
+ t[0x33] = "three";
+ t[0x34] = "four";
+ t[0x35] = "five";
+ t[0x36] = "six";
+ t[0x37] = "seven";
+ t[0x38] = "eight";
+ t[0x39] = "nine";
+ t[0x3A] = "colon";
+ t[0x3B] = "semicolon";
+ t[0x3C] = "less";
+ t[0x3D] = "equal";
+ t[0x3E] = "greater";
+ t[0x3F] = "question";
+ t[0x40] = "congruent";
+ t[0x41] = "Alpha";
+ t[0x42] = "Beta";
+ t[0x43] = "Chi";
+ t[0x44] = "Delta";
+ t[0x45] = "Epsilon";
+ t[0x46] = "Phi";
+ t[0x47] = "Gamma";
+ t[0x48] = "Eta";
+ t[0x49] = "Iota";
+ t[0x4A] = "theta1";
+ t[0x4B] = "Kappa";
+ t[0x4C] = "Lambda";
+ t[0x4D] = "Mu";
+ t[0x4E] = "Nu";
+ t[0x4F] = "Omicron";
+ t[0x50] = "Pi";
+ t[0x51] = "Theta";
+ t[0x52] = "Rho";
+ t[0x53] = "Sigma";
+ t[0x54] = "Tau";
+ t[0x55] = "Upsilon";
+ t[0x56] = "sigma1";
+ t[0x57] = "Omega";
+ t[0x58] = "Xi";
+ t[0x59] = "Psi";
+ t[0x5A] = "Zeta";
+ t[0x5B] = "bracketleft";
+ t[0x5C] = "therefore";
+ t[0x5D] = "bracketright";
+ t[0x5E] = "perpendicular";
+ t[0x5F] = "underscore";
+ t[0x60] = "radicalex";
+ t[0x61] = "alpha";
+ t[0x62] = "beta";
+ t[0x63] = "chi";
+ t[0x64] = "delta";
+ t[0x65] = "epsilon";
+ t[0x66] = "phi";
+ t[0x67] = "gamma";
+ t[0x68] = "eta";
+ t[0x69] = "iota";
+ t[0x6A] = "phi1";
+ t[0x6B] = "kappa";
+ t[0x6C] = "lambda";
+ t[0x6D] = "mu";
+ t[0x6E] = "nu";
+ t[0x6F] = "omicron";
+ t[0x70] = "pi";
+ t[0x71] = "theta";
+ t[0x72] = "rho";
+ t[0x73] = "sigma";
+ t[0x74] = "tau";
+ t[0x75] = "upsilon";
+ t[0x76] = "omega1";
+ t[0x77] = "omega";
+ t[0x78] = "xi";
+ t[0x79] = "psi";
+ t[0x7A] = "zeta";
+ t[0x7B] = "braceleft";
+ t[0x7C] = "bar";
+ t[0x7D] = "braceright";
+ t[0x7E] = "similar";
+ t[0xA0] = "Euro";
+ t[0xA1] = "Upsilon1";
+ t[0xA2] = "minute";
+ t[0xA3] = "lessequal";
+ t[0xA4] = "fraction";
+ t[0xA5] = "infinity";
+ t[0xA6] = "florin";
+ t[0xA7] = "club";
+ t[0xA8] = "diamond";
+ t[0xA9] = "heart";
+ t[0xAA] = "spade";
+ t[0xAB] = "arrowboth";
+ t[0xAC] = "arrowleft";
+ t[0xAD] = "arrowup";
+ t[0xAE] = "arrowright";
+ t[0xAF] = "arrowdown";
+ t[0xB0] = "degree";
+ t[0xB1] = "plusminus";
+ t[0xB2] = "second";
+ t[0xB3] = "greaterequal";
+ t[0xB4] = "multiply";
+ t[0xB5] = "proportional";
+ t[0xB6] = "partialdiff";
+ t[0xB7] = "bullet";
+ t[0xB8] = "divide";
+ t[0xB9] = "notequal";
+ t[0xBA] = "equivalence";
+ t[0xBB] = "approxequal";
+ t[0xBC] = "ellipsis";
+ t[0xBD] = "arrowvertex";
+ t[0xBE] = "arrowhorizex";
+ t[0xBF] = "carriagereturn";
+ t[0xC0] = "aleph";
+ t[0xC1] = "Ifraktur";
+ t[0xC2] = "Rfraktur";
+ t[0xC3] = "weierstrass";
+ t[0xC4] = "circlemultiply";
+ t[0xC5] = "circleplus";
+ t[0xC6] = "emptyset";
+ t[0xC7] = "intersection";
+ t[0xC8] = "union";
+ t[0xC9] = "propersuperset";
+ t[0xCA] = "reflexsuperset";
+ t[0xCB] = "notsubset";
+ t[0xCC] = "propersubset";
+ t[0xCD] = "reflexsubset";
+ t[0xCE] = "element";
+ t[0xCF] = "notelement";
+ t[0xD0] = "angle";
+ t[0xD1] = "gradient";
+ t[0xD2] = "registerserif";
+ t[0xD3] = "copyrightserif";
+ t[0xD4] = "trademarkserif";
+ t[0xD5] = "product";
+ t[0xD6] = "radical";
+ t[0xD7] = "dotmath";
+ t[0xD8] = "logicalnot";
+ t[0xD9] = "logicaland";
+ t[0xDA] = "logicalor";
+ t[0xDB] = "arrowdblboth";
+ t[0xDC] = "arrowdblleft";
+ t[0xDD] = "arrowdblup";
+ t[0xDE] = "arrowdblright";
+ t[0xDF] = "arrowdbldown";
+ t[0xE0] = "lozenge";
+ t[0xE1] = "angleleft";
+ t[0xE2] = "registersans";
+ t[0xE3] = "copyrightsans";
+ t[0xE4] = "trademarksans";
+ t[0xE5] = "summation";
+ t[0xE6] = "parenlefttp";
+ t[0xE7] = "parenleftex";
+ t[0xE8] = "parenleftbt";
+ t[0xE9] = "bracketlefttp";
+ t[0xEA] = "bracketleftex";
+ t[0xEB] = "bracketleftbt";
+ t[0xEC] = "bracelefttp";
+ t[0xED] = "braceleftmid";
+ t[0xEE] = "braceleftbt";
+ t[0xEF] = "braceex";
+ t[0xF1] = "angleright";
+ t[0xF2] = "integral";
+ t[0xF3] = "integraltp";
+ t[0xF4] = "integralex";
+ t[0xF5] = "integralbt";
+ t[0xF6] = "parenrighttp";
+ t[0xF7] = "parenrightex";
+ t[0xF8] = "parenrightbt";
+ t[0xF9] = "bracketrighttp";
+ t[0xFA] = "bracketrightex";
+ t[0xFB] = "bracketrightbt";
+ t[0xFC] = "bracerighttp";
+ t[0xFD] = "bracerightmid";
+ t[0xFE] = "bracerightbt";
+ return t;
+ }
+
+ private static string?[] BuildEncoding_zapfDingbats()
+ {
+ var t = new string?[256];
+ t[0x20] = "space";
+ t[0x21] = "a1";
+ t[0x22] = "a2";
+ t[0x23] = "a202";
+ t[0x24] = "a3";
+ t[0x25] = "a4";
+ t[0x26] = "a5";
+ t[0x27] = "a119";
+ t[0x28] = "a118";
+ t[0x29] = "a117";
+ t[0x2A] = "a11";
+ t[0x2B] = "a12";
+ t[0x2C] = "a13";
+ t[0x2D] = "a14";
+ t[0x2E] = "a15";
+ t[0x2F] = "a16";
+ t[0x30] = "a105";
+ t[0x31] = "a17";
+ t[0x32] = "a18";
+ t[0x33] = "a19";
+ t[0x34] = "a20";
+ t[0x35] = "a21";
+ t[0x36] = "a22";
+ t[0x37] = "a23";
+ t[0x38] = "a24";
+ t[0x39] = "a25";
+ t[0x3A] = "a26";
+ t[0x3B] = "a27";
+ t[0x3C] = "a28";
+ t[0x3D] = "a6";
+ t[0x3E] = "a7";
+ t[0x3F] = "a8";
+ t[0x40] = "a9";
+ t[0x41] = "a10";
+ t[0x42] = "a29";
+ t[0x43] = "a30";
+ t[0x44] = "a31";
+ t[0x45] = "a32";
+ t[0x46] = "a33";
+ t[0x47] = "a34";
+ t[0x48] = "a35";
+ t[0x49] = "a36";
+ t[0x4A] = "a37";
+ t[0x4B] = "a38";
+ t[0x4C] = "a39";
+ t[0x4D] = "a40";
+ t[0x4E] = "a41";
+ t[0x4F] = "a42";
+ t[0x50] = "a43";
+ t[0x51] = "a44";
+ t[0x52] = "a45";
+ t[0x53] = "a46";
+ t[0x54] = "a47";
+ t[0x55] = "a48";
+ t[0x56] = "a49";
+ t[0x57] = "a50";
+ t[0x58] = "a51";
+ t[0x59] = "a52";
+ t[0x5A] = "a53";
+ t[0x5B] = "a54";
+ t[0x5C] = "a55";
+ t[0x5D] = "a56";
+ t[0x5E] = "a57";
+ t[0x5F] = "a58";
+ t[0x60] = "a59";
+ t[0x61] = "a60";
+ t[0x62] = "a61";
+ t[0x63] = "a62";
+ t[0x64] = "a63";
+ t[0x65] = "a64";
+ t[0x66] = "a65";
+ t[0x67] = "a66";
+ t[0x68] = "a67";
+ t[0x69] = "a68";
+ t[0x6A] = "a69";
+ t[0x6B] = "a70";
+ t[0x6C] = "a71";
+ t[0x6D] = "a72";
+ t[0x6E] = "a73";
+ t[0x6F] = "a74";
+ t[0x70] = "a203";
+ t[0x71] = "a75";
+ t[0x72] = "a204";
+ t[0x73] = "a76";
+ t[0x74] = "a77";
+ t[0x75] = "a78";
+ t[0x76] = "a79";
+ t[0x77] = "a81";
+ t[0x78] = "a82";
+ t[0x79] = "a83";
+ t[0x7A] = "a84";
+ t[0x7B] = "a97";
+ t[0x7C] = "a98";
+ t[0x7D] = "a99";
+ t[0x7E] = "a100";
+ t[0x80] = "a89";
+ t[0x81] = "a90";
+ t[0x82] = "a93";
+ t[0x83] = "a94";
+ t[0x84] = "a91";
+ t[0x85] = "a92";
+ t[0x86] = "a205";
+ t[0x87] = "a85";
+ t[0x88] = "a206";
+ t[0x89] = "a86";
+ t[0x8A] = "a87";
+ t[0x8B] = "a88";
+ t[0x8C] = "a95";
+ t[0x8D] = "a96";
+ t[0xA1] = "a101";
+ t[0xA2] = "a102";
+ t[0xA3] = "a103";
+ t[0xA4] = "a104";
+ t[0xA5] = "a106";
+ t[0xA6] = "a107";
+ t[0xA7] = "a108";
+ t[0xA8] = "a112";
+ t[0xA9] = "a111";
+ t[0xAA] = "a110";
+ t[0xAB] = "a109";
+ t[0xAC] = "a120";
+ t[0xAD] = "a121";
+ t[0xAE] = "a122";
+ t[0xAF] = "a123";
+ t[0xB0] = "a124";
+ t[0xB1] = "a125";
+ t[0xB2] = "a126";
+ t[0xB3] = "a127";
+ t[0xB4] = "a128";
+ t[0xB5] = "a129";
+ t[0xB6] = "a130";
+ t[0xB7] = "a131";
+ t[0xB8] = "a132";
+ t[0xB9] = "a133";
+ t[0xBA] = "a134";
+ t[0xBB] = "a135";
+ t[0xBC] = "a136";
+ t[0xBD] = "a137";
+ t[0xBE] = "a138";
+ t[0xBF] = "a139";
+ t[0xC0] = "a140";
+ t[0xC1] = "a141";
+ t[0xC2] = "a142";
+ t[0xC3] = "a143";
+ t[0xC4] = "a144";
+ t[0xC5] = "a145";
+ t[0xC6] = "a146";
+ t[0xC7] = "a147";
+ t[0xC8] = "a148";
+ t[0xC9] = "a149";
+ t[0xCA] = "a150";
+ t[0xCB] = "a151";
+ t[0xCC] = "a152";
+ t[0xCD] = "a153";
+ t[0xCE] = "a154";
+ t[0xCF] = "a155";
+ t[0xD0] = "a156";
+ t[0xD1] = "a157";
+ t[0xD2] = "a158";
+ t[0xD3] = "a159";
+ t[0xD4] = "a160";
+ t[0xD5] = "a161";
+ t[0xD6] = "a163";
+ t[0xD7] = "a164";
+ t[0xD8] = "a196";
+ t[0xD9] = "a165";
+ t[0xDA] = "a192";
+ t[0xDB] = "a166";
+ t[0xDC] = "a167";
+ t[0xDD] = "a168";
+ t[0xDE] = "a169";
+ t[0xDF] = "a170";
+ t[0xE0] = "a171";
+ t[0xE1] = "a172";
+ t[0xE2] = "a173";
+ t[0xE3] = "a162";
+ t[0xE4] = "a174";
+ t[0xE5] = "a175";
+ t[0xE6] = "a176";
+ t[0xE7] = "a177";
+ t[0xE8] = "a178";
+ t[0xE9] = "a179";
+ t[0xEA] = "a193";
+ t[0xEB] = "a180";
+ t[0xEC] = "a199";
+ t[0xED] = "a181";
+ t[0xEE] = "a200";
+ t[0xEF] = "a182";
+ t[0xF1] = "a201";
+ t[0xF2] = "a183";
+ t[0xF3] = "a184";
+ t[0xF4] = "a197";
+ t[0xF5] = "a185";
+ t[0xF6] = "a194";
+ t[0xF7] = "a198";
+ t[0xF8] = "a186";
+ t[0xF9] = "a195";
+ t[0xFA] = "a187";
+ t[0xFB] = "a188";
+ t[0xFC] = "a189";
+ t[0xFD] = "a190";
+ t[0xFE] = "a191";
+ return t;
+ }
+}
diff --git a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs
new file mode 100644
index 00000000..d8a3b328
--- /dev/null
+++ b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs
@@ -0,0 +1,76 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Reflection;
+
+namespace VellumPdf.Reader.Fonts;
+
+///
+/// Maps a ZapfDingbats glyph name (a1, a2, ...) to Unicode, for a Symbol-flag font
+/// whose base font resolves to ZapfDingbats (see step 8). Backed by
+/// the embedded ZapfDingbatsGlyphList.txt resource: the Adobe AGL repository's own
+/// zapfdingbats.txt (BSD-3-Clause; see NOTICE), normalised the same way
+/// eng/generate-symbol-font-metrics.py normalises the AFM files it reads, and committed with
+/// its #-comment header intact.
+///
+///
+/// The file carries 201 name-to-codepoint lines, one for every ZapfDingbats.afm glyph name
+/// except space (which needs no lookup: it is U+0020 under every encoding this reader
+/// builds). That includes the 14 names assigned to the codes SymbolFontMetrics' own
+/// remarks name as ZapfDingbats.afm-only (0x80 through 0x8D: a85 through
+/// a96, a205, a206). Those 14 names carry ordinary Unicode mappings in
+/// Adobe's own zapfdingbats.txt (the ornamental-bracket block, U+2768–U+2775), which the
+/// Adobe Glyph List proper does not list; omitting them here would leave those codes with no
+/// Unicode route at all, which SimpleFontReaderTests pins directly against 0x80.
+///
+internal static class ZapfDingbatsGlyphList
+{
+ private static readonly Lazy> _map = new(Load, isThreadSafe: true);
+
+ /// Entry count of the loaded list: test-only visibility for pinning its size (201)
+ /// directly, rather than through behaviour.
+ internal static int Count => _map.Value.Count;
+
+ ///
+ /// Maps (a ZapfDingbats glyph name, verbatim, no uniXXXX or
+ /// _-composition) to its Unicode code point. Returns when the
+ /// name is not in the list.
+ ///
+ public static bool TryMap(string name, out string unicode)
+ {
+ if (_map.Value.TryGetValue(name, out var mapped))
+ {
+ unicode = mapped;
+ return true;
+ }
+ unicode = "";
+ return false;
+ }
+
+ private static Dictionary Load()
+ {
+ var map = new Dictionary(210, StringComparer.Ordinal);
+ var asm = Assembly.GetExecutingAssembly();
+ using var stream = asm.GetManifestResourceStream("ZapfDingbatsGlyphList.txt");
+ if (stream is null)
+ return map;
+
+ using var reader = new StreamReader(stream, System.Text.Encoding.ASCII, detectEncodingFromByteOrderMarks: false);
+ string? line;
+ while ((line = reader.ReadLine()) is not null)
+ {
+ if (line.Length == 0 || line[0] == '#')
+ continue;
+ var semi = line.IndexOf(';');
+ if (semi <= 0 || semi >= line.Length - 1)
+ continue;
+ if (int.TryParse(line[(semi + 1)..], System.Globalization.NumberStyles.HexNumber, null, out var cp))
+ {
+ // Unguarded: ZapfDingbatsGlyphList.txt is a pinned embedded resource (NOTICE
+ // records its source commit and SHA-256), never a surrogate half in practice.
+ map[line[..semi]] = char.ConvertFromUtf32(cp);
+ }
+ }
+ return map;
+ }
+}
diff --git a/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs
new file mode 100644
index 00000000..70aa8a27
--- /dev/null
+++ b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs
@@ -0,0 +1,96 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using VellumPdf.Core;
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader;
+
+public sealed partial class PdfDocumentReader
+{
+ private readonly FontCache _fontCache = new();
+
+ ///
+ /// Builds (or returns the cached) for a /Font resource
+ /// entry. is the raw value from a resource dictionary's
+ /// /Font subdictionary (an indirect reference, or, unusually, a direct dictionary),
+ /// resolved here before its /Subtype is read.
+ ///
+ ///
+ /// Throws when this reader is disposed, checked before
+ /// anything else, the same as every other entry point on this class.
+ ///
+ /// Returns silently, with no diagnostic, for /Subtype /Type0 and
+ /// /Subtype /Type3: readers for those are not built yet (#98), and reporting
+ /// here would fire on every CJK or Type 3
+ /// document until they are. Not yet wired to ContentInterpreter, so the only callers
+ /// today are tests.
+ ///
+ ///
+ /// Both resolves this method does of its own (the font entry itself, then its /Subtype)
+ /// go through a single / for
+ /// : throws past
+ /// MaxResolveDepth when parsing a stream's own structure re-enters resolution (a
+ /// font entry naming a stream whose /Length chains through indirect references deeply
+ /// enough), and that can happen before is ever reached
+ /// to catch it with its own equivalent guard. Caught here, it reports the same
+ /// and returns
+ /// instead of letting the exception escape.
+ ///
+ ///
+ internal PdfFontReader? GetFontReader(PdfObject rawFontEntry, DiagnosticSink sink, int? pageIndex)
+ {
+ ThrowIfDisposed();
+
+ int? objectNumber = null;
+ int? generation = null;
+ if (rawFontEntry is PdfIndirectReference r)
+ {
+ objectNumber = r.ObjectNumber;
+ generation = r.Generation;
+ }
+
+ PdfDictionary fontDict;
+ PdfName? subtype;
+ try
+ {
+ if (ResolveValue(rawFontEntry) is not PdfDictionary resolved)
+ {
+ sink.Report(
+ PdfReaderDiagnosticCode.FontUnreadable, "the font resource is not a dictionary.",
+ objectNumber, generation, pageIndex);
+ return null;
+ }
+ fontDict = resolved;
+
+ var subtypeRaw = fontDict.Get(PdfName.Subtype);
+ subtype = (subtypeRaw is null ? null : ResolveValue(subtypeRaw)) as PdfName;
+ }
+ catch (InvalidDataException)
+ {
+ sink.Report(
+ PdfReaderDiagnosticCode.FontUnreadable,
+ "building this font hit the reader's own indirect-object resolution depth limit.",
+ objectNumber, generation, pageIndex);
+ return null;
+ }
+
+ switch (subtype?.Value)
+ {
+ case "Type1" or "MMType1" or "TrueType":
+ return _fontCache.GetOrCreate(
+ objectNumber, generation,
+ () => SimpleFontReader.Create(this, fontDict, objectNumber, generation, sink, pageIndex));
+
+ case "Type0" or "Type3":
+ return null;
+
+ default:
+ sink.Report(
+ PdfReaderDiagnosticCode.FontUnreadable,
+ "/Subtype is missing or names a font type this reader does not know.",
+ objectNumber, generation, pageIndex);
+ return null;
+ }
+ }
+}
diff --git a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs
index 293f6ff3..f3a78fa2 100644
--- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs
+++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs
@@ -562,6 +562,56 @@ public enum PdfReaderDiagnosticCode
///
ContentLimitExceeded = 309,
+ // ── 4xx: fonts and Unicode mapping ──────────────────────────────────────────────────────────
+
+ ///
+ /// A simple font's resource dictionary was not a dictionary at all, had no usable
+ /// /BaseFont, named a /Subtype this reader knows nothing about (not
+ /// /Type0 or /Type3, which are silent until this reader gains readers for them),
+ /// or the reader could not resolve the font dictionary or one of its entries because building
+ /// it hit this reader's own indirect-object resolution depth limit. ISO 32000-2 §9.6.2.1
+ /// Table 109 makes /Type, /Subtype and /BaseFont all required entries of
+ /// a font dictionary. Reported once per font.
+ ///
+ FontUnreadable = 400,
+
+ ///
+ /// A simple font's /Encoding (ISO 32000-2 §9.6.5) was neither a known encoding name nor
+ /// an encoding dictionary, its /BaseEncoding named an encoding this reader does not
+ /// know or was itself an unresolved indirect reference, its /Differences was present
+ /// but not an array, or a /Differences element was out of range, named a glyph longer
+ /// than this reader's own name-length bound, was of a type §9.6.5.1 does not permit there
+ /// (only integers and names), or was an indirect reference, which §7.3.10 permits but this
+ /// reader does not follow inside the array. Every element condition except the over-long name
+ /// stops the array from being applied any further. Reported once per font.
+ ///
+ FontEncodingMalformed = 401,
+
+ ///
+ /// A simple font's /FirstChar, /LastChar, or /Widths (ISO 32000-2 Table
+ /// 109) was missing, mistyped, holding a non-number element, out of range, or shorter than
+ /// LastChar - FirstChar + 1 requires, or the font had no /Widths at all and is
+ /// not one of the standard 14 fonts (§9.6.2.1). A malformed /Widths is not repaired
+ /// from the standard 14 font's own AFM metrics even when the font is one of them; every code
+ /// keeps MissingWidth. Reported once per font.
+ ///
+ FontWidthsMalformed = 402,
+
+ ///
+ /// No code in this font has a route to Unicode: it names no /ToUnicode stream, and no
+ /// glyph name its encoding assigns is one the Adobe Glyph List, or the ZapfDingbats glyph
+ /// list, maps (§9.10.2). Reported once per font.
+ ///
+ FontNoUnicodeRoute = 403,
+
+ ///
+ /// A glyph was decoded whose character code has no Unicode mapping, while at least one other
+ /// code in the same font does. Reported once per font, on the first such glyph decoded. Not
+ /// reported while the font names a /ToUnicode stream this reader does not parse yet,
+ /// since that stream may map the code (§9.10.2 gives it priority over the glyph-name route).
+ ///
+ UnmappedGlyphs = 404,
+
// ── 9xx: reserved ───────────────────────────────────────────────────────────────────────────
///
@@ -631,6 +681,11 @@ internal static class PdfReaderDiagnosticSeverities
PdfReaderDiagnosticCode.InlineImageMalformed => PdfReaderDiagnosticSeverity.Warning,
PdfReaderDiagnosticCode.ContentStreamTooLarge => PdfReaderDiagnosticSeverity.Warning,
PdfReaderDiagnosticCode.ContentLimitExceeded => PdfReaderDiagnosticSeverity.Warning,
+ PdfReaderDiagnosticCode.FontUnreadable => PdfReaderDiagnosticSeverity.Warning,
+ PdfReaderDiagnosticCode.FontEncodingMalformed => PdfReaderDiagnosticSeverity.Warning,
+ PdfReaderDiagnosticCode.FontWidthsMalformed => PdfReaderDiagnosticSeverity.Warning,
+ PdfReaderDiagnosticCode.FontNoUnicodeRoute => PdfReaderDiagnosticSeverity.Info,
+ PdfReaderDiagnosticCode.UnmappedGlyphs => PdfReaderDiagnosticSeverity.Info,
PdfReaderDiagnosticCode.DiagnosticsSuppressed => PdfReaderDiagnosticSeverity.Warning,
_ => throw new UnreachableException($"No severity is mapped for {code}."),
};
diff --git a/src/VellumPdf.Reader/PublicAPI.Unshipped.txt b/src/VellumPdf.Reader/PublicAPI.Unshipped.txt
index f54877a3..a280524b 100644
--- a/src/VellumPdf.Reader/PublicAPI.Unshipped.txt
+++ b/src/VellumPdf.Reader/PublicAPI.Unshipped.txt
@@ -46,6 +46,10 @@ VellumPdf.Reader.PdfReaderDiagnosticCode.DiagnosticsSuppressed = 900 -> VellumPd
VellumPdf.Reader.PdfReaderDiagnosticCode.FilterArrayElementNotName = 106 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.FilterNull = 105 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.FilterValueMalformed = 107 -> VellumPdf.Reader.PdfReaderDiagnosticCode
+VellumPdf.Reader.PdfReaderDiagnosticCode.FontEncodingMalformed = 401 -> VellumPdf.Reader.PdfReaderDiagnosticCode
+VellumPdf.Reader.PdfReaderDiagnosticCode.FontNoUnicodeRoute = 403 -> VellumPdf.Reader.PdfReaderDiagnosticCode
+VellumPdf.Reader.PdfReaderDiagnosticCode.FontUnreadable = 400 -> VellumPdf.Reader.PdfReaderDiagnosticCode
+VellumPdf.Reader.PdfReaderDiagnosticCode.FontWidthsMalformed = 402 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.FormXObjectBudgetExceeded = 305 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.FormXObjectCycle = 304 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.FormXObjectDepthExceeded = 303 -> VellumPdf.Reader.PdfReaderDiagnosticCode
@@ -66,6 +70,7 @@ VellumPdf.Reader.PdfReaderDiagnosticCode.PageTreeNodeMalformed = 206 -> VellumPd
VellumPdf.Reader.PdfReaderDiagnosticCode.ResourceMissing = 306 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.UnknownFilter = 110 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.UnknownOperator = 301 -> VellumPdf.Reader.PdfReaderDiagnosticCode
+VellumPdf.Reader.PdfReaderDiagnosticCode.UnmappedGlyphs = 404 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.UnsupportedPredictor = 109 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticCode.XrefReconstructed = 100 -> VellumPdf.Reader.PdfReaderDiagnosticCode
VellumPdf.Reader.PdfReaderDiagnosticSeverity
diff --git a/src/VellumPdf.Reader/Resources/AdobeGlyphList.txt b/src/VellumPdf.Reader/Resources/AdobeGlyphList.txt
new file mode 100644
index 00000000..d7ad8f27
--- /dev/null
+++ b/src/VellumPdf.Reader/Resources/AdobeGlyphList.txt
@@ -0,0 +1,4282 @@
+.notdef 0000
+A 0041
+AE 00C6
+AEacute 01FC
+AEmacron 01E2
+AEsmall F7E6
+Aacute 00C1
+Aacutesmall F7E1
+Abreve 0102
+Abreveacute 1EAE
+Abrevecyrillic 04D0
+Abrevedotbelow 1EB6
+Abrevegrave 1EB0
+Abrevehookabove 1EB2
+Abrevetilde 1EB4
+Acaron 01CD
+Acircle 24B6
+Acircumflex 00C2
+Acircumflexacute 1EA4
+Acircumflexdotbelow 1EAC
+Acircumflexgrave 1EA6
+Acircumflexhookabove 1EA8
+Acircumflexsmall F7E2
+Acircumflextilde 1EAA
+Acute F6C9
+Acutesmall F7B4
+Acyrillic 0410
+Adblgrave 0200
+Adieresis 00C4
+Adieresiscyrillic 04D2
+Adieresismacron 01DE
+Adieresissmall F7E4
+Adotbelow 1EA0
+Adotmacron 01E0
+Agrave 00C0
+Agravesmall F7E0
+Ahookabove 1EA2
+Aiecyrillic 04D4
+Ainvertedbreve 0202
+Alpha 0391
+Alphatonos 0386
+Amacron 0100
+Amonospace FF21
+Aogonek 0104
+Aring 00C5
+Aringacute 01FA
+Aringbelow 1E00
+Aringsmall F7E5
+Asmall F761
+Atilde 00C3
+Atildesmall F7E3
+Aybarmenian 0531
+B 0042
+Bcircle 24B7
+Bdotaccent 1E02
+Bdotbelow 1E04
+Becyrillic 0411
+Benarmenian 0532
+Beta 0392
+Bhook 0181
+Blinebelow 1E06
+Bmonospace FF22
+Brevesmall F6F4
+Bsmall F762
+Btopbar 0182
+C 0043
+Caarmenian 053E
+Cacute 0106
+Caron F6CA
+Caronsmall F6F5
+Ccaron 010C
+Ccedilla 00C7
+Ccedillaacute 1E08
+Ccedillasmall F7E7
+Ccircle 24B8
+Ccircumflex 0108
+Cdot 010A
+Cdotaccent 010A
+Cedillasmall F7B8
+Chaarmenian 0549
+Cheabkhasiancyrillic 04BC
+Checyrillic 0427
+Chedescenderabkhasiancyrillic 04BE
+Chedescendercyrillic 04B6
+Chedieresiscyrillic 04F4
+Cheharmenian 0543
+Chekhakassiancyrillic 04CB
+Cheverticalstrokecyrillic 04B8
+Chi 03A7
+Chook 0187
+Circumflexsmall F6F6
+Cmonospace FF23
+Coarmenian 0551
+Csmall F763
+D 0044
+DZ 01F1
+DZcaron 01C4
+Daarmenian 0534
+Dafrican 0189
+Dcaron 010E
+Dcedilla 1E10
+Dcircle 24B9
+Dcircumflexbelow 1E12
+Dcroat 0110
+Ddotaccent 1E0A
+Ddotbelow 1E0C
+Decyrillic 0414
+Deicoptic 03EE
+Delta 2206
+Deltagreek 0394
+Dhook 018A
+Dieresis F6CB
+DieresisAcute F6CC
+DieresisGrave F6CD
+Dieresissmall F7A8
+Digammagreek 03DC
+Djecyrillic 0402
+Dlinebelow 1E0E
+Dmonospace FF24
+Dotaccentsmall F6F7
+Dslash 0110
+Dsmall F764
+Dtopbar 018B
+Dz 01F2
+Dzcaron 01C5
+Dzeabkhasiancyrillic 04E0
+Dzecyrillic 0405
+Dzhecyrillic 040F
+E 0045
+Eacute 00C9
+Eacutesmall F7E9
+Ebreve 0114
+Ecaron 011A
+Ecedillabreve 1E1C
+Echarmenian 0535
+Ecircle 24BA
+Ecircumflex 00CA
+Ecircumflexacute 1EBE
+Ecircumflexbelow 1E18
+Ecircumflexdotbelow 1EC6
+Ecircumflexgrave 1EC0
+Ecircumflexhookabove 1EC2
+Ecircumflexsmall F7EA
+Ecircumflextilde 1EC4
+Ecyrillic 0404
+Edblgrave 0204
+Edieresis 00CB
+Edieresissmall F7EB
+Edot 0116
+Edotaccent 0116
+Edotbelow 1EB8
+Efcyrillic 0424
+Egrave 00C8
+Egravesmall F7E8
+Eharmenian 0537
+Ehookabove 1EBA
+Eightroman 2167
+Einvertedbreve 0206
+Eiotifiedcyrillic 0464
+Elcyrillic 041B
+Elevenroman 216A
+Emacron 0112
+Emacronacute 1E16
+Emacrongrave 1E14
+Emcyrillic 041C
+Emonospace FF25
+Encyrillic 041D
+Endescendercyrillic 04A2
+Eng 014A
+Enghecyrillic 04A4
+Enhookcyrillic 04C7
+Eogonek 0118
+Eopen 0190
+Epsilon 0395
+Epsilontonos 0388
+Ercyrillic 0420
+Ereversed 018E
+Ereversedcyrillic 042D
+Escyrillic 0421
+Esdescendercyrillic 04AA
+Esh 01A9
+Esmall F765
+Eta 0397
+Etarmenian 0538
+Etatonos 0389
+Eth 00D0
+Ethsmall F7F0
+Etilde 1EBC
+Etildebelow 1E1A
+Euro 20AC
+Ezh 01B7
+Ezhcaron 01EE
+Ezhreversed 01B8
+F 0046
+Fcircle 24BB
+Fdotaccent 1E1E
+Feharmenian 0556
+Feicoptic 03E4
+Fhook 0191
+Fitacyrillic 0472
+Fiveroman 2164
+Fmonospace FF26
+Fourroman 2163
+Fsmall F766
+G 0047
+GBsquare 3387
+Gacute 01F4
+Gamma 0393
+Gammaafrican 0194
+Gangiacoptic 03EA
+Gbreve 011E
+Gcaron 01E6
+Gcedilla 0122
+Gcircle 24BC
+Gcircumflex 011C
+Gcommaaccent 0122
+Gdot 0120
+Gdotaccent 0120
+Gecyrillic 0413
+Ghadarmenian 0542
+Ghemiddlehookcyrillic 0494
+Ghestrokecyrillic 0492
+Gheupturncyrillic 0490
+Ghook 0193
+Gimarmenian 0533
+Gjecyrillic 0403
+Gmacron 1E20
+Gmonospace FF27
+Grave F6CE
+Gravesmall F760
+Gsmall F767
+Gsmallhook 029B
+Gstroke 01E4
+H 0048
+H18533 25CF
+H18543 25AA
+H18551 25AB
+H22073 25A1
+HPsquare 33CB
+Haabkhasiancyrillic 04A8
+Hadescendercyrillic 04B2
+Hardsigncyrillic 042A
+Hbar 0126
+Hbrevebelow 1E2A
+Hcedilla 1E28
+Hcircle 24BD
+Hcircumflex 0124
+Hdieresis 1E26
+Hdotaccent 1E22
+Hdotbelow 1E24
+Hmonospace FF28
+Hoarmenian 0540
+Horicoptic 03E8
+Hsmall F768
+Hungarumlaut F6CF
+Hungarumlautsmall F6F8
+Hzsquare 3390
+I 0049
+IAcyrillic 042F
+IJ 0132
+IUcyrillic 042E
+Iacute 00CD
+Iacutesmall F7ED
+Ibreve 012C
+Icaron 01CF
+Icircle 24BE
+Icircumflex 00CE
+Icircumflexsmall F7EE
+Icyrillic 0406
+Idblgrave 0208
+Idieresis 00CF
+Idieresisacute 1E2E
+Idieresiscyrillic 04E4
+Idieresissmall F7EF
+Idot 0130
+Idotaccent 0130
+Idotbelow 1ECA
+Iebrevecyrillic 04D6
+Iecyrillic 0415
+Ifraktur 2111
+Igrave 00CC
+Igravesmall F7EC
+Ihookabove 1EC8
+Iicyrillic 0418
+Iinvertedbreve 020A
+Iishortcyrillic 0419
+Imacron 012A
+Imacroncyrillic 04E2
+Imonospace FF29
+Iniarmenian 053B
+Iocyrillic 0401
+Iogonek 012E
+Iota 0399
+Iotaafrican 0196
+Iotadieresis 03AA
+Iotatonos 038A
+Ismall F769
+Istroke 0197
+Itilde 0128
+Itildebelow 1E2C
+Izhitsacyrillic 0474
+Izhitsadblgravecyrillic 0476
+J 004A
+Jaarmenian 0541
+Jcircle 24BF
+Jcircumflex 0134
+Jecyrillic 0408
+Jheharmenian 054B
+Jmonospace FF2A
+Jsmall F76A
+K 004B
+KBsquare 3385
+KKsquare 33CD
+Kabashkircyrillic 04A0
+Kacute 1E30
+Kacyrillic 041A
+Kadescendercyrillic 049A
+Kahookcyrillic 04C3
+Kappa 039A
+Kastrokecyrillic 049E
+Kaverticalstrokecyrillic 049C
+Kcaron 01E8
+Kcedilla 0136
+Kcircle 24C0
+Kcommaaccent 0136
+Kdotbelow 1E32
+Keharmenian 0554
+Kenarmenian 053F
+Khacyrillic 0425
+Kheicoptic 03E6
+Khook 0198
+Kjecyrillic 040C
+Klinebelow 1E34
+Kmonospace FF2B
+Koppacyrillic 0480
+Koppagreek 03DE
+Ksicyrillic 046E
+Ksmall F76B
+L 004C
+LJ 01C7
+LL F6BF
+Lacute 0139
+Lambda 039B
+Lcaron 013D
+Lcedilla 013B
+Lcircle 24C1
+Lcircumflexbelow 1E3C
+Lcommaaccent 013B
+Ldot 013F
+Ldotaccent 013F
+Ldotbelow 1E36
+Ldotbelowmacron 1E38
+Liwnarmenian 053C
+Lj 01C8
+Ljecyrillic 0409
+Llinebelow 1E3A
+Lmonospace FF2C
+Lslash 0141
+Lslashsmall F6F9
+Lsmall F76C
+M 004D
+MBsquare 3386
+Macron F6D0
+Macronsmall F7AF
+Macute 1E3E
+Mcircle 24C2
+Mdotaccent 1E40
+Mdotbelow 1E42
+Menarmenian 0544
+Mmonospace FF2D
+Msmall F76D
+Mturned 019C
+Mu 039C
+N 004E
+NJ 01CA
+Nacute 0143
+Ncaron 0147
+Ncedilla 0145
+Ncircle 24C3
+Ncircumflexbelow 1E4A
+Ncommaaccent 0145
+Ndotaccent 1E44
+Ndotbelow 1E46
+Nhookleft 019D
+Nineroman 2168
+Nj 01CB
+Njecyrillic 040A
+Nlinebelow 1E48
+Nmonospace FF2E
+Nowarmenian 0546
+Nsmall F76E
+Ntilde 00D1
+Ntildesmall F7F1
+Nu 039D
+O 004F
+OE 0152
+OEsmall F6FA
+Oacute 00D3
+Oacutesmall F7F3
+Obarredcyrillic 04E8
+Obarreddieresiscyrillic 04EA
+Obreve 014E
+Ocaron 01D1
+Ocenteredtilde 019F
+Ocircle 24C4
+Ocircumflex 00D4
+Ocircumflexacute 1ED0
+Ocircumflexdotbelow 1ED8
+Ocircumflexgrave 1ED2
+Ocircumflexhookabove 1ED4
+Ocircumflexsmall F7F4
+Ocircumflextilde 1ED6
+Ocyrillic 041E
+Odblacute 0150
+Odblgrave 020C
+Odieresis 00D6
+Odieresiscyrillic 04E6
+Odieresissmall F7F6
+Odotbelow 1ECC
+Ogoneksmall F6FB
+Ograve 00D2
+Ogravesmall F7F2
+Oharmenian 0555
+Ohm 2126
+Ohookabove 1ECE
+Ohorn 01A0
+Ohornacute 1EDA
+Ohorndotbelow 1EE2
+Ohorngrave 1EDC
+Ohornhookabove 1EDE
+Ohorntilde 1EE0
+Ohungarumlaut 0150
+Oi 01A2
+Oinvertedbreve 020E
+Omacron 014C
+Omacronacute 1E52
+Omacrongrave 1E50
+Omega 2126
+Omegacyrillic 0460
+Omegagreek 03A9
+Omegaroundcyrillic 047A
+Omegatitlocyrillic 047C
+Omegatonos 038F
+Omicron 039F
+Omicrontonos 038C
+Omonospace FF2F
+Oneroman 2160
+Oogonek 01EA
+Oogonekmacron 01EC
+Oopen 0186
+Oslash 00D8
+Oslashacute 01FE
+Oslashsmall F7F8
+Osmall F76F
+Ostrokeacute 01FE
+Otcyrillic 047E
+Otilde 00D5
+Otildeacute 1E4C
+Otildedieresis 1E4E
+Otildesmall F7F5
+P 0050
+Pacute 1E54
+Pcircle 24C5
+Pdotaccent 1E56
+Pecyrillic 041F
+Peharmenian 054A
+Pemiddlehookcyrillic 04A6
+Phi 03A6
+Phook 01A4
+Pi 03A0
+Piwrarmenian 0553
+Pmonospace FF30
+Psi 03A8
+Psicyrillic 0470
+Psmall F770
+Q 0051
+Qcircle 24C6
+Qmonospace FF31
+Qsmall F771
+R 0052
+Raarmenian 054C
+Racute 0154
+Rcaron 0158
+Rcedilla 0156
+Rcircle 24C7
+Rcommaaccent 0156
+Rdblgrave 0210
+Rdotaccent 1E58
+Rdotbelow 1E5A
+Rdotbelowmacron 1E5C
+Reharmenian 0550
+Rfraktur 211C
+Rho 03A1
+Ringsmall F6FC
+Rinvertedbreve 0212
+Rlinebelow 1E5E
+Rmonospace FF32
+Rsmall F772
+Rsmallinverted 0281
+Rsmallinvertedsuperior 02B6
+S 0053
+SF010000 250C
+SF020000 2514
+SF030000 2510
+SF040000 2518
+SF050000 253C
+SF060000 252C
+SF070000 2534
+SF080000 251C
+SF090000 2524
+SF100000 2500
+SF110000 2502
+SF190000 2561
+SF200000 2562
+SF210000 2556
+SF220000 2555
+SF230000 2563
+SF240000 2551
+SF250000 2557
+SF260000 255D
+SF270000 255C
+SF280000 255B
+SF360000 255E
+SF370000 255F
+SF380000 255A
+SF390000 2554
+SF400000 2569
+SF410000 2566
+SF420000 2560
+SF430000 2550
+SF440000 256C
+SF450000 2567
+SF460000 2568
+SF470000 2564
+SF480000 2565
+SF490000 2559
+SF500000 2558
+SF510000 2552
+SF520000 2553
+SF530000 256B
+SF540000 256A
+Sacute 015A
+Sacutedotaccent 1E64
+Sampigreek 03E0
+Scaron 0160
+Scarondotaccent 1E66
+Scaronsmall F6FD
+Scedilla 015E
+Schwa 018F
+Schwacyrillic 04D8
+Schwadieresiscyrillic 04DA
+Scircle 24C8
+Scircumflex 015C
+Scommaaccent 0218
+Sdotaccent 1E60
+Sdotbelow 1E62
+Sdotbelowdotaccent 1E68
+Seharmenian 054D
+Sevenroman 2166
+Shaarmenian 0547
+Shacyrillic 0428
+Shchacyrillic 0429
+Sheicoptic 03E2
+Shhacyrillic 04BA
+Shimacoptic 03EC
+Sigma 03A3
+Sixroman 2165
+Smonospace FF33
+Softsigncyrillic 042C
+Ssmall F773
+Stigmagreek 03DA
+T 0054
+Tau 03A4
+Tbar 0166
+Tcaron 0164
+Tcedilla 0162
+Tcircle 24C9
+Tcircumflexbelow 1E70
+Tcommaaccent 0162
+Tdotaccent 1E6A
+Tdotbelow 1E6C
+Tecyrillic 0422
+Tedescendercyrillic 04AC
+Tenroman 2169
+Tetsecyrillic 04B4
+Theta 0398
+Thook 01AC
+Thorn 00DE
+Thornsmall F7FE
+Threeroman 2162
+Tildesmall F6FE
+Tiwnarmenian 054F
+Tlinebelow 1E6E
+Tmonospace FF34
+Toarmenian 0539
+Tonefive 01BC
+Tonesix 0184
+Tonetwo 01A7
+Tretroflexhook 01AE
+Tsecyrillic 0426
+Tshecyrillic 040B
+Tsmall F774
+Twelveroman 216B
+Tworoman 2161
+U 0055
+Uacute 00DA
+Uacutesmall F7FA
+Ubreve 016C
+Ucaron 01D3
+Ucircle 24CA
+Ucircumflex 00DB
+Ucircumflexbelow 1E76
+Ucircumflexsmall F7FB
+Ucyrillic 0423
+Udblacute 0170
+Udblgrave 0214
+Udieresis 00DC
+Udieresisacute 01D7
+Udieresisbelow 1E72
+Udieresiscaron 01D9
+Udieresiscyrillic 04F0
+Udieresisgrave 01DB
+Udieresismacron 01D5
+Udieresissmall F7FC
+Udotbelow 1EE4
+Ugrave 00D9
+Ugravesmall F7F9
+Uhookabove 1EE6
+Uhorn 01AF
+Uhornacute 1EE8
+Uhorndotbelow 1EF0
+Uhorngrave 1EEA
+Uhornhookabove 1EEC
+Uhorntilde 1EEE
+Uhungarumlaut 0170
+Uhungarumlautcyrillic 04F2
+Uinvertedbreve 0216
+Ukcyrillic 0478
+Umacron 016A
+Umacroncyrillic 04EE
+Umacrondieresis 1E7A
+Umonospace FF35
+Uogonek 0172
+Upsilon 03A5
+Upsilon1 03D2
+Upsilonacutehooksymbolgreek 03D3
+Upsilonafrican 01B1
+Upsilondieresis 03AB
+Upsilondieresishooksymbolgreek 03D4
+Upsilonhooksymbol 03D2
+Upsilontonos 038E
+Uring 016E
+Ushortcyrillic 040E
+Usmall F775
+Ustraightcyrillic 04AE
+Ustraightstrokecyrillic 04B0
+Utilde 0168
+Utildeacute 1E78
+Utildebelow 1E74
+V 0056
+Vcircle 24CB
+Vdotbelow 1E7E
+Vecyrillic 0412
+Vewarmenian 054E
+Vhook 01B2
+Vmonospace FF36
+Voarmenian 0548
+Vsmall F776
+Vtilde 1E7C
+W 0057
+Wacute 1E82
+Wcircle 24CC
+Wcircumflex 0174
+Wdieresis 1E84
+Wdotaccent 1E86
+Wdotbelow 1E88
+Wgrave 1E80
+Wmonospace FF37
+Wsmall F777
+X 0058
+Xcircle 24CD
+Xdieresis 1E8C
+Xdotaccent 1E8A
+Xeharmenian 053D
+Xi 039E
+Xmonospace FF38
+Xsmall F778
+Y 0059
+Yacute 00DD
+Yacutesmall F7FD
+Yatcyrillic 0462
+Ycircle 24CE
+Ycircumflex 0176
+Ydieresis 0178
+Ydieresissmall F7FF
+Ydotaccent 1E8E
+Ydotbelow 1EF4
+Yericyrillic 042B
+Yerudieresiscyrillic 04F8
+Ygrave 1EF2
+Yhook 01B3
+Yhookabove 1EF6
+Yiarmenian 0545
+Yicyrillic 0407
+Yiwnarmenian 0552
+Ymonospace FF39
+Ysmall F779
+Ytilde 1EF8
+Yusbigcyrillic 046A
+Yusbigiotifiedcyrillic 046C
+Yuslittlecyrillic 0466
+Yuslittleiotifiedcyrillic 0468
+Z 005A
+Zaarmenian 0536
+Zacute 0179
+Zcaron 017D
+Zcaronsmall F6FF
+Zcircle 24CF
+Zcircumflex 1E90
+Zdot 017B
+Zdotaccent 017B
+Zdotbelow 1E92
+Zecyrillic 0417
+Zedescendercyrillic 0498
+Zedieresiscyrillic 04DE
+Zeta 0396
+Zhearmenian 053A
+Zhebrevecyrillic 04C1
+Zhecyrillic 0416
+Zhedescendercyrillic 0496
+Zhedieresiscyrillic 04DC
+Zlinebelow 1E94
+Zmonospace FF3A
+Zsmall F77A
+Zstroke 01B5
+a 0061
+aabengali 0986
+aacute 00E1
+aadeva 0906
+aagujarati 0A86
+aagurmukhi 0A06
+aamatragurmukhi 0A3E
+aarusquare 3303
+aavowelsignbengali 09BE
+aavowelsigndeva 093E
+aavowelsigngujarati 0ABE
+abbreviationmarkarmenian 055F
+abbreviationsigndeva 0970
+abengali 0985
+abopomofo 311A
+abreve 0103
+abreveacute 1EAF
+abrevecyrillic 04D1
+abrevedotbelow 1EB7
+abrevegrave 1EB1
+abrevehookabove 1EB3
+abrevetilde 1EB5
+acaron 01CE
+acircle 24D0
+acircumflex 00E2
+acircumflexacute 1EA5
+acircumflexdotbelow 1EAD
+acircumflexgrave 1EA7
+acircumflexhookabove 1EA9
+acircumflextilde 1EAB
+acute 00B4
+acutebelowcmb 0317
+acutecmb 0301
+acutecomb 0301
+acutedeva 0954
+acutelowmod 02CF
+acutetonecmb 0341
+acyrillic 0430
+adblgrave 0201
+addakgurmukhi 0A71
+adeva 0905
+adieresis 00E4
+adieresiscyrillic 04D3
+adieresismacron 01DF
+adotbelow 1EA1
+adotmacron 01E1
+ae 00E6
+aeacute 01FD
+aekorean 3150
+aemacron 01E3
+afii00208 2015
+afii08941 20A4
+afii10017 0410
+afii10018 0411
+afii10019 0412
+afii10020 0413
+afii10021 0414
+afii10022 0415
+afii10023 0401
+afii10024 0416
+afii10025 0417
+afii10026 0418
+afii10027 0419
+afii10028 041A
+afii10029 041B
+afii10030 041C
+afii10031 041D
+afii10032 041E
+afii10033 041F
+afii10034 0420
+afii10035 0421
+afii10036 0422
+afii10037 0423
+afii10038 0424
+afii10039 0425
+afii10040 0426
+afii10041 0427
+afii10042 0428
+afii10043 0429
+afii10044 042A
+afii10045 042B
+afii10046 042C
+afii10047 042D
+afii10048 042E
+afii10049 042F
+afii10050 0490
+afii10051 0402
+afii10052 0403
+afii10053 0404
+afii10054 0405
+afii10055 0406
+afii10056 0407
+afii10057 0408
+afii10058 0409
+afii10059 040A
+afii10060 040B
+afii10061 040C
+afii10062 040E
+afii10063 F6C4
+afii10064 F6C5
+afii10065 0430
+afii10066 0431
+afii10067 0432
+afii10068 0433
+afii10069 0434
+afii10070 0435
+afii10071 0451
+afii10072 0436
+afii10073 0437
+afii10074 0438
+afii10075 0439
+afii10076 043A
+afii10077 043B
+afii10078 043C
+afii10079 043D
+afii10080 043E
+afii10081 043F
+afii10082 0440
+afii10083 0441
+afii10084 0442
+afii10085 0443
+afii10086 0444
+afii10087 0445
+afii10088 0446
+afii10089 0447
+afii10090 0448
+afii10091 0449
+afii10092 044A
+afii10093 044B
+afii10094 044C
+afii10095 044D
+afii10096 044E
+afii10097 044F
+afii10098 0491
+afii10099 0452
+afii10100 0453
+afii10101 0454
+afii10102 0455
+afii10103 0456
+afii10104 0457
+afii10105 0458
+afii10106 0459
+afii10107 045A
+afii10108 045B
+afii10109 045C
+afii10110 045E
+afii10145 040F
+afii10146 0462
+afii10147 0472
+afii10148 0474
+afii10192 F6C6
+afii10193 045F
+afii10194 0463
+afii10195 0473
+afii10196 0475
+afii10831 F6C7
+afii10832 F6C8
+afii10846 04D9
+afii299 200E
+afii300 200F
+afii301 200D
+afii57381 066A
+afii57388 060C
+afii57392 0660
+afii57393 0661
+afii57394 0662
+afii57395 0663
+afii57396 0664
+afii57397 0665
+afii57398 0666
+afii57399 0667
+afii57400 0668
+afii57401 0669
+afii57403 061B
+afii57407 061F
+afii57409 0621
+afii57410 0622
+afii57411 0623
+afii57412 0624
+afii57413 0625
+afii57414 0626
+afii57415 0627
+afii57416 0628
+afii57417 0629
+afii57418 062A
+afii57419 062B
+afii57420 062C
+afii57421 062D
+afii57422 062E
+afii57423 062F
+afii57424 0630
+afii57425 0631
+afii57426 0632
+afii57427 0633
+afii57428 0634
+afii57429 0635
+afii57430 0636
+afii57431 0637
+afii57432 0638
+afii57433 0639
+afii57434 063A
+afii57440 0640
+afii57441 0641
+afii57442 0642
+afii57443 0643
+afii57444 0644
+afii57445 0645
+afii57446 0646
+afii57448 0648
+afii57449 0649
+afii57450 064A
+afii57451 064B
+afii57452 064C
+afii57453 064D
+afii57454 064E
+afii57455 064F
+afii57456 0650
+afii57457 0651
+afii57458 0652
+afii57470 0647
+afii57505 06A4
+afii57506 067E
+afii57507 0686
+afii57508 0698
+afii57509 06AF
+afii57511 0679
+afii57512 0688
+afii57513 0691
+afii57514 06BA
+afii57519 06D2
+afii57534 06D5
+afii57636 20AA
+afii57645 05BE
+afii57658 05C3
+afii57664 05D0
+afii57665 05D1
+afii57666 05D2
+afii57667 05D3
+afii57668 05D4
+afii57669 05D5
+afii57670 05D6
+afii57671 05D7
+afii57672 05D8
+afii57673 05D9
+afii57674 05DA
+afii57675 05DB
+afii57676 05DC
+afii57677 05DD
+afii57678 05DE
+afii57679 05DF
+afii57680 05E0
+afii57681 05E1
+afii57682 05E2
+afii57683 05E3
+afii57684 05E4
+afii57685 05E5
+afii57686 05E6
+afii57687 05E7
+afii57688 05E8
+afii57689 05E9
+afii57690 05EA
+afii57694 FB2A
+afii57695 FB2B
+afii57700 FB4B
+afii57705 FB1F
+afii57716 05F0
+afii57717 05F1
+afii57718 05F2
+afii57723 FB35
+afii57793 05B4
+afii57794 05B5
+afii57795 05B6
+afii57796 05BB
+afii57797 05B8
+afii57798 05B7
+afii57799 05B0
+afii57800 05B2
+afii57801 05B1
+afii57802 05B3
+afii57803 05C2
+afii57804 05C1
+afii57806 05B9
+afii57807 05BC
+afii57839 05BD
+afii57841 05BF
+afii57842 05C0
+afii57929 02BC
+afii61248 2105
+afii61289 2113
+afii61352 2116
+afii61573 202C
+afii61574 202D
+afii61575 202E
+afii61664 200C
+afii63167 066D
+afii64937 02BD
+agrave 00E0
+agujarati 0A85
+agurmukhi 0A05
+ahiragana 3042
+ahookabove 1EA3
+aibengali 0990
+aibopomofo 311E
+aideva 0910
+aiecyrillic 04D5
+aigujarati 0A90
+aigurmukhi 0A10
+aimatragurmukhi 0A48
+ainarabic 0639
+ainfinalarabic FECA
+aininitialarabic FECB
+ainmedialarabic FECC
+ainvertedbreve 0203
+aivowelsignbengali 09C8
+aivowelsigndeva 0948
+aivowelsigngujarati 0AC8
+akatakana 30A2
+akatakanahalfwidth FF71
+akorean 314F
+alef 05D0
+alefarabic 0627
+alefdageshhebrew FB30
+aleffinalarabic FE8E
+alefhamzaabovearabic 0623
+alefhamzaabovefinalarabic FE84
+alefhamzabelowarabic 0625
+alefhamzabelowfinalarabic FE88
+alefhebrew 05D0
+aleflamedhebrew FB4F
+alefmaddaabovearabic 0622
+alefmaddaabovefinalarabic FE82
+alefmaksuraarabic 0649
+alefmaksurafinalarabic FEF0
+alefmaksurainitialarabic FEF3
+alefmaksuramedialarabic FEF4
+alefpatahhebrew FB2E
+alefqamatshebrew FB2F
+aleph 2135
+allequal 224C
+alpha 03B1
+alphatonos 03AC
+amacron 0101
+amonospace FF41
+ampersand 0026
+ampersandmonospace FF06
+ampersandsmall F726
+amsquare 33C2
+anbopomofo 3122
+angbopomofo 3124
+angkhankhuthai 0E5A
+angle 2220
+anglebracketleft 3008
+anglebracketleftvertical FE3F
+anglebracketright 3009
+anglebracketrightvertical FE40
+angleleft 2329
+angleright 232A
+angstrom 212B
+anoteleia 0387
+anudattadeva 0952
+anusvarabengali 0982
+anusvaradeva 0902
+anusvaragujarati 0A82
+aogonek 0105
+apaatosquare 3300
+aparen 249C
+apostrophearmenian 055A
+apostrophemod 02BC
+apple F8FF
+approaches 2250
+approxequal 2248
+approxequalorimage 2252
+approximatelyequal 2245
+araeaekorean 318E
+araeakorean 318D
+arc 2312
+arighthalfring 1E9A
+aring 00E5
+aringacute 01FB
+aringbelow 1E01
+arrowboth 2194
+arrowdashdown 21E3
+arrowdashleft 21E0
+arrowdashright 21E2
+arrowdashup 21E1
+arrowdblboth 21D4
+arrowdbldown 21D3
+arrowdblleft 21D0
+arrowdblright 21D2
+arrowdblup 21D1
+arrowdown 2193
+arrowdownleft 2199
+arrowdownright 2198
+arrowdownwhite 21E9
+arrowheaddownmod 02C5
+arrowheadleftmod 02C2
+arrowheadrightmod 02C3
+arrowheadupmod 02C4
+arrowhorizex F8E7
+arrowleft 2190
+arrowleftdbl 21D0
+arrowleftdblstroke 21CD
+arrowleftoverright 21C6
+arrowleftwhite 21E6
+arrowright 2192
+arrowrightdblstroke 21CF
+arrowrightheavy 279E
+arrowrightoverleft 21C4
+arrowrightwhite 21E8
+arrowtableft 21E4
+arrowtabright 21E5
+arrowup 2191
+arrowupdn 2195
+arrowupdnbse 21A8
+arrowupdownbase 21A8
+arrowupleft 2196
+arrowupleftofdown 21C5
+arrowupright 2197
+arrowupwhite 21E7
+arrowvertex F8E6
+asciicircum 005E
+asciicircummonospace FF3E
+asciitilde 007E
+asciitildemonospace FF5E
+ascript 0251
+ascriptturned 0252
+asmallhiragana 3041
+asmallkatakana 30A1
+asmallkatakanahalfwidth FF67
+asterisk 002A
+asteriskaltonearabic 066D
+asteriskarabic 066D
+asteriskmath 2217
+asteriskmonospace FF0A
+asterisksmall FE61
+asterism 2042
+asuperior F6E9
+asymptoticallyequal 2243
+at 0040
+atilde 00E3
+atmonospace FF20
+atsmall FE6B
+aturned 0250
+aubengali 0994
+aubopomofo 3120
+audeva 0914
+augujarati 0A94
+augurmukhi 0A14
+aulengthmarkbengali 09D7
+aumatragurmukhi 0A4C
+auvowelsignbengali 09CC
+auvowelsigndeva 094C
+auvowelsigngujarati 0ACC
+avagrahadeva 093D
+aybarmenian 0561
+ayin 05E2
+ayinaltonehebrew FB20
+ayinhebrew 05E2
+b 0062
+babengali 09AC
+backslash 005C
+backslashmonospace FF3C
+badeva 092C
+bagujarati 0AAC
+bagurmukhi 0A2C
+bahiragana 3070
+bahtthai 0E3F
+bakatakana 30D0
+bar 007C
+barmonospace FF5C
+bbopomofo 3105
+bcircle 24D1
+bdotaccent 1E03
+bdotbelow 1E05
+beamedsixteenthnotes 266C
+because 2235
+becyrillic 0431
+beharabic 0628
+behfinalarabic FE90
+behinitialarabic FE91
+behiragana 3079
+behmedialarabic FE92
+behmeeminitialarabic FC9F
+behmeemisolatedarabic FC08
+behnoonfinalarabic FC6D
+bekatakana 30D9
+benarmenian 0562
+bet 05D1
+beta 03B2
+betasymbolgreek 03D0
+betdagesh FB31
+betdageshhebrew FB31
+bethebrew 05D1
+betrafehebrew FB4C
+bhabengali 09AD
+bhadeva 092D
+bhagujarati 0AAD
+bhagurmukhi 0A2D
+bhook 0253
+bihiragana 3073
+bikatakana 30D3
+bilabialclick 0298
+bindigurmukhi 0A02
+birusquare 3331
+blackcircle 25CF
+blackdiamond 25C6
+blackdownpointingtriangle 25BC
+blackleftpointingpointer 25C4
+blackleftpointingtriangle 25C0
+blacklenticularbracketleft 3010
+blacklenticularbracketleftvertical FE3B
+blacklenticularbracketright 3011
+blacklenticularbracketrightvertical FE3C
+blacklowerlefttriangle 25E3
+blacklowerrighttriangle 25E2
+blackrectangle 25AC
+blackrightpointingpointer 25BA
+blackrightpointingtriangle 25B6
+blacksmallsquare 25AA
+blacksmilingface 263B
+blacksquare 25A0
+blackstar 2605
+blackupperlefttriangle 25E4
+blackupperrighttriangle 25E5
+blackuppointingsmalltriangle 25B4
+blackuppointingtriangle 25B2
+blank 2423
+blinebelow 1E07
+block 2588
+bmonospace FF42
+bobaimaithai 0E1A
+bohiragana 307C
+bokatakana 30DC
+bparen 249D
+bqsquare 33C3
+braceex F8F4
+braceleft 007B
+braceleftbt F8F3
+braceleftmid F8F2
+braceleftmonospace FF5B
+braceleftsmall FE5B
+bracelefttp F8F1
+braceleftvertical FE37
+braceright 007D
+bracerightbt F8FE
+bracerightmid F8FD
+bracerightmonospace FF5D
+bracerightsmall FE5C
+bracerighttp F8FC
+bracerightvertical FE38
+bracketleft 005B
+bracketleftbt F8F0
+bracketleftex F8EF
+bracketleftmonospace FF3B
+bracketlefttp F8EE
+bracketright 005D
+bracketrightbt F8FB
+bracketrightex F8FA
+bracketrightmonospace FF3D
+bracketrighttp F8F9
+breve 02D8
+brevebelowcmb 032E
+brevecmb 0306
+breveinvertedbelowcmb 032F
+breveinvertedcmb 0311
+breveinverteddoublecmb 0361
+bridgebelowcmb 032A
+bridgeinvertedbelowcmb 033A
+brokenbar 00A6
+bstroke 0180
+bsuperior F6EA
+btopbar 0183
+buhiragana 3076
+bukatakana 30D6
+bullet 2022
+bulletinverse 25D8
+bulletoperator 2219
+bullseye 25CE
+c 0063
+caarmenian 056E
+cabengali 099A
+cacute 0107
+cadeva 091A
+cagujarati 0A9A
+cagurmukhi 0A1A
+calsquare 3388
+candrabindubengali 0981
+candrabinducmb 0310
+candrabindudeva 0901
+candrabindugujarati 0A81
+capslock 21EA
+careof 2105
+caron 02C7
+caronbelowcmb 032C
+caroncmb 030C
+carriagereturn 21B5
+cbopomofo 3118
+ccaron 010D
+ccedilla 00E7
+ccedillaacute 1E09
+ccircle 24D2
+ccircumflex 0109
+ccurl 0255
+cdot 010B
+cdotaccent 010B
+cdsquare 33C5
+cedilla 00B8
+cedillacmb 0327
+cent 00A2
+centigrade 2103
+centinferior F6DF
+centmonospace FFE0
+centoldstyle F7A2
+centsuperior F6E0
+chaarmenian 0579
+chabengali 099B
+chadeva 091B
+chagujarati 0A9B
+chagurmukhi 0A1B
+chbopomofo 3114
+cheabkhasiancyrillic 04BD
+checkmark 2713
+checyrillic 0447
+chedescenderabkhasiancyrillic 04BF
+chedescendercyrillic 04B7
+chedieresiscyrillic 04F5
+cheharmenian 0573
+chekhakassiancyrillic 04CC
+cheverticalstrokecyrillic 04B9
+chi 03C7
+chieuchacirclekorean 3277
+chieuchaparenkorean 3217
+chieuchcirclekorean 3269
+chieuchkorean 314A
+chieuchparenkorean 3209
+chochangthai 0E0A
+chochanthai 0E08
+chochingthai 0E09
+chochoethai 0E0C
+chook 0188
+cieucacirclekorean 3276
+cieucaparenkorean 3216
+cieuccirclekorean 3268
+cieuckorean 3148
+cieucparenkorean 3208
+cieucuparenkorean 321C
+circle 25CB
+circlemultiply 2297
+circleot 2299
+circleplus 2295
+circlepostalmark 3036
+circlewithlefthalfblack 25D0
+circlewithrighthalfblack 25D1
+circumflex 02C6
+circumflexbelowcmb 032D
+circumflexcmb 0302
+clear 2327
+clickalveolar 01C2
+clickdental 01C0
+clicklateral 01C1
+clickretroflex 01C3
+club 2663
+clubsuitblack 2663
+clubsuitwhite 2667
+cmcubedsquare 33A4
+cmonospace FF43
+cmsquaredsquare 33A0
+coarmenian 0581
+colon 003A
+colonmonetary 20A1
+colonmonospace FF1A
+colonsign 20A1
+colonsmall FE55
+colontriangularhalfmod 02D1
+colontriangularmod 02D0
+comma 002C
+commaabovecmb 0313
+commaaboverightcmb 0315
+commaaccent F6C3
+commaarabic 060C
+commaarmenian 055D
+commainferior F6E1
+commamonospace FF0C
+commareversedabovecmb 0314
+commareversedmod 02BD
+commasmall FE50
+commasuperior F6E2
+commaturnedabovecmb 0312
+commaturnedmod 02BB
+compass 263C
+congruent 2245
+contourintegral 222E
+control 2303
+controlACK 0006
+controlBEL 0007
+controlBS 0008
+controlCAN 0018
+controlCR 000D
+controlDC1 0011
+controlDC2 0012
+controlDC3 0013
+controlDC4 0014
+controlDEL 007F
+controlDLE 0010
+controlEM 0019
+controlENQ 0005
+controlEOT 0004
+controlESC 001B
+controlETB 0017
+controlETX 0003
+controlFF 000C
+controlFS 001C
+controlGS 001D
+controlHT 0009
+controlLF 000A
+controlNAK 0015
+controlRS 001E
+controlSI 000F
+controlSO 000E
+controlSOT 0002
+controlSTX 0001
+controlSUB 001A
+controlSYN 0016
+controlUS 001F
+controlVT 000B
+copyright 00A9
+copyrightsans F8E9
+copyrightserif F6D9
+cornerbracketleft 300C
+cornerbracketlefthalfwidth FF62
+cornerbracketleftvertical FE41
+cornerbracketright 300D
+cornerbracketrighthalfwidth FF63
+cornerbracketrightvertical FE42
+corporationsquare 337F
+cosquare 33C7
+coverkgsquare 33C6
+cparen 249E
+cruzeiro 20A2
+cstretched 0297
+curlyand 22CF
+curlyor 22CE
+currency 00A4
+cyrBreve F6D1
+cyrFlex F6D2
+cyrbreve F6D4
+cyrflex F6D5
+d 0064
+daarmenian 0564
+dabengali 09A6
+dadarabic 0636
+dadeva 0926
+dadfinalarabic FEBE
+dadinitialarabic FEBF
+dadmedialarabic FEC0
+dagesh 05BC
+dageshhebrew 05BC
+dagger 2020
+daggerdbl 2021
+dagujarati 0AA6
+dagurmukhi 0A26
+dahiragana 3060
+dakatakana 30C0
+dalarabic 062F
+dalet 05D3
+daletdagesh FB33
+daletdageshhebrew FB33
+dalethatafpatah 05D3 05B2
+dalethatafpatahhebrew 05D3 05B2
+dalethatafsegol 05D3 05B1
+dalethatafsegolhebrew 05D3 05B1
+dalethebrew 05D3
+dalethiriq 05D3 05B4
+dalethiriqhebrew 05D3 05B4
+daletholam 05D3 05B9
+daletholamhebrew 05D3 05B9
+daletpatah 05D3 05B7
+daletpatahhebrew 05D3 05B7
+daletqamats 05D3 05B8
+daletqamatshebrew 05D3 05B8
+daletqubuts 05D3 05BB
+daletqubutshebrew 05D3 05BB
+daletsegol 05D3 05B6
+daletsegolhebrew 05D3 05B6
+daletsheva 05D3 05B0
+daletshevahebrew 05D3 05B0
+dalettsere 05D3 05B5
+dalettserehebrew 05D3 05B5
+dalfinalarabic FEAA
+dammaarabic 064F
+dammalowarabic 064F
+dammatanaltonearabic 064C
+dammatanarabic 064C
+danda 0964
+dargahebrew 05A7
+dargalefthebrew 05A7
+dasiapneumatacyrilliccmb 0485
+dblGrave F6D3
+dblanglebracketleft 300A
+dblanglebracketleftvertical FE3D
+dblanglebracketright 300B
+dblanglebracketrightvertical FE3E
+dblarchinvertedbelowcmb 032B
+dblarrowleft 21D4
+dblarrowright 21D2
+dbldanda 0965
+dblgrave F6D6
+dblgravecmb 030F
+dblintegral 222C
+dbllowline 2017
+dbllowlinecmb 0333
+dbloverlinecmb 033F
+dblprimemod 02BA
+dblverticalbar 2016
+dblverticallineabovecmb 030E
+dbopomofo 3109
+dbsquare 33C8
+dcaron 010F
+dcedilla 1E11
+dcircle 24D3
+dcircumflexbelow 1E13
+dcroat 0111
+ddabengali 09A1
+ddadeva 0921
+ddagujarati 0AA1
+ddagurmukhi 0A21
+ddalarabic 0688
+ddalfinalarabic FB89
+dddhadeva 095C
+ddhabengali 09A2
+ddhadeva 0922
+ddhagujarati 0AA2
+ddhagurmukhi 0A22
+ddotaccent 1E0B
+ddotbelow 1E0D
+decimalseparatorarabic 066B
+decimalseparatorpersian 066B
+decyrillic 0434
+degree 00B0
+dehihebrew 05AD
+dehiragana 3067
+deicoptic 03EF
+dekatakana 30C7
+deleteleft 232B
+deleteright 2326
+delta 03B4
+deltaturned 018D
+denominatorminusonenumeratorbengali 09F8
+dezh 02A4
+dhabengali 09A7
+dhadeva 0927
+dhagujarati 0AA7
+dhagurmukhi 0A27
+dhook 0257
+dialytikatonos 0385
+dialytikatonoscmb 0344
+diamond 2666
+diamondsuitwhite 2662
+dieresis 00A8
+dieresisacute F6D7
+dieresisbelowcmb 0324
+dieresiscmb 0308
+dieresisgrave F6D8
+dieresistonos 0385
+dihiragana 3062
+dikatakana 30C2
+dittomark 3003
+divide 00F7
+divides 2223
+divisionslash 2215
+djecyrillic 0452
+dkshade 2593
+dlinebelow 1E0F
+dlsquare 3397
+dmacron 0111
+dmonospace FF44
+dnblock 2584
+dochadathai 0E0E
+dodekthai 0E14
+dohiragana 3069
+dokatakana 30C9
+dollar 0024
+dollarinferior F6E3
+dollarmonospace FF04
+dollaroldstyle F724
+dollarsmall FE69
+dollarsuperior F6E4
+dong 20AB
+dorusquare 3326
+dotaccent 02D9
+dotaccentcmb 0307
+dotbelowcmb 0323
+dotbelowcomb 0323
+dotkatakana 30FB
+dotlessi 0131
+dotlessj F6BE
+dotlessjstrokehook 0284
+dotmath 22C5
+dottedcircle 25CC
+doubleyodpatah FB1F
+doubleyodpatahhebrew FB1F
+downtackbelowcmb 031E
+downtackmod 02D5
+dparen 249F
+dsuperior F6EB
+dtail 0256
+dtopbar 018C
+duhiragana 3065
+dukatakana 30C5
+dz 01F3
+dzaltone 02A3
+dzcaron 01C6
+dzcurl 02A5
+dzeabkhasiancyrillic 04E1
+dzecyrillic 0455
+dzhecyrillic 045F
+e 0065
+eacute 00E9
+earth 2641
+ebengali 098F
+ebopomofo 311C
+ebreve 0115
+ecandradeva 090D
+ecandragujarati 0A8D
+ecandravowelsigndeva 0945
+ecandravowelsigngujarati 0AC5
+ecaron 011B
+ecedillabreve 1E1D
+echarmenian 0565
+echyiwnarmenian 0587
+ecircle 24D4
+ecircumflex 00EA
+ecircumflexacute 1EBF
+ecircumflexbelow 1E19
+ecircumflexdotbelow 1EC7
+ecircumflexgrave 1EC1
+ecircumflexhookabove 1EC3
+ecircumflextilde 1EC5
+ecyrillic 0454
+edblgrave 0205
+edeva 090F
+edieresis 00EB
+edot 0117
+edotaccent 0117
+edotbelow 1EB9
+eegurmukhi 0A0F
+eematragurmukhi 0A47
+efcyrillic 0444
+egrave 00E8
+egujarati 0A8F
+eharmenian 0567
+ehbopomofo 311D
+ehiragana 3048
+ehookabove 1EBB
+eibopomofo 311F
+eight 0038
+eightarabic 0668
+eightbengali 09EE
+eightcircle 2467
+eightcircleinversesansserif 2791
+eightdeva 096E
+eighteencircle 2471
+eighteenparen 2485
+eighteenperiod 2499
+eightgujarati 0AEE
+eightgurmukhi 0A6E
+eighthackarabic 0668
+eighthangzhou 3028
+eighthnotebeamed 266B
+eightideographicparen 3227
+eightinferior 2088
+eightmonospace FF18
+eightoldstyle F738
+eightparen 247B
+eightperiod 248F
+eightpersian 06F8
+eightroman 2177
+eightsuperior 2078
+eightthai 0E58
+einvertedbreve 0207
+eiotifiedcyrillic 0465
+ekatakana 30A8
+ekatakanahalfwidth FF74
+ekonkargurmukhi 0A74
+ekorean 3154
+elcyrillic 043B
+element 2208
+elevencircle 246A
+elevenparen 247E
+elevenperiod 2492
+elevenroman 217A
+ellipsis 2026
+ellipsisvertical 22EE
+emacron 0113
+emacronacute 1E17
+emacrongrave 1E15
+emcyrillic 043C
+emdash 2014
+emdashvertical FE31
+emonospace FF45
+emphasismarkarmenian 055B
+emptyset 2205
+enbopomofo 3123
+encyrillic 043D
+endash 2013
+endashvertical FE32
+endescendercyrillic 04A3
+eng 014B
+engbopomofo 3125
+enghecyrillic 04A5
+enhookcyrillic 04C8
+enspace 2002
+eogonek 0119
+eokorean 3153
+eopen 025B
+eopenclosed 029A
+eopenreversed 025C
+eopenreversedclosed 025E
+eopenreversedhook 025D
+eparen 24A0
+epsilon 03B5
+epsilontonos 03AD
+equal 003D
+equalmonospace FF1D
+equalsmall FE66
+equalsuperior 207C
+equivalence 2261
+erbopomofo 3126
+ercyrillic 0440
+ereversed 0258
+ereversedcyrillic 044D
+escyrillic 0441
+esdescendercyrillic 04AB
+esh 0283
+eshcurl 0286
+eshortdeva 090E
+eshortvowelsigndeva 0946
+eshreversedloop 01AA
+eshsquatreversed 0285
+esmallhiragana 3047
+esmallkatakana 30A7
+esmallkatakanahalfwidth FF6A
+estimated 212E
+esuperior F6EC
+eta 03B7
+etarmenian 0568
+etatonos 03AE
+eth 00F0
+etilde 1EBD
+etildebelow 1E1B
+etnahtafoukhhebrew 0591
+etnahtafoukhlefthebrew 0591
+etnahtahebrew 0591
+etnahtalefthebrew 0591
+eturned 01DD
+eukorean 3161
+euro 20AC
+evowelsignbengali 09C7
+evowelsigndeva 0947
+evowelsigngujarati 0AC7
+exclam 0021
+exclamarmenian 055C
+exclamdbl 203C
+exclamdown 00A1
+exclamdownsmall F7A1
+exclammonospace FF01
+exclamsmall F721
+existential 2203
+ezh 0292
+ezhcaron 01EF
+ezhcurl 0293
+ezhreversed 01B9
+ezhtail 01BA
+f 0066
+fadeva 095E
+fagurmukhi 0A5E
+fahrenheit 2109
+fathaarabic 064E
+fathalowarabic 064E
+fathatanarabic 064B
+fbopomofo 3108
+fcircle 24D5
+fdotaccent 1E1F
+feharabic 0641
+feharmenian 0586
+fehfinalarabic FED2
+fehinitialarabic FED3
+fehmedialarabic FED4
+feicoptic 03E5
+female 2640
+ff FB00
+ffi FB03
+ffl FB04
+fi FB01
+fifteencircle 246E
+fifteenparen 2482
+fifteenperiod 2496
+figuredash 2012
+filledbox 25A0
+filledrect 25AC
+finalkaf 05DA
+finalkafdagesh FB3A
+finalkafdageshhebrew FB3A
+finalkafhebrew 05DA
+finalkafqamats 05DA 05B8
+finalkafqamatshebrew 05DA 05B8
+finalkafsheva 05DA 05B0
+finalkafshevahebrew 05DA 05B0
+finalmem 05DD
+finalmemhebrew 05DD
+finalnun 05DF
+finalnunhebrew 05DF
+finalpe 05E3
+finalpehebrew 05E3
+finaltsadi 05E5
+finaltsadihebrew 05E5
+firsttonechinese 02C9
+fisheye 25C9
+fitacyrillic 0473
+five 0035
+fivearabic 0665
+fivebengali 09EB
+fivecircle 2464
+fivecircleinversesansserif 278E
+fivedeva 096B
+fiveeighths 215D
+fivegujarati 0AEB
+fivegurmukhi 0A6B
+fivehackarabic 0665
+fivehangzhou 3025
+fiveideographicparen 3224
+fiveinferior 2085
+fivemonospace FF15
+fiveoldstyle F735
+fiveparen 2478
+fiveperiod 248C
+fivepersian 06F5
+fiveroman 2174
+fivesuperior 2075
+fivethai 0E55
+fl FB02
+florin 0192
+fmonospace FF46
+fmsquare 3399
+fofanthai 0E1F
+fofathai 0E1D
+fongmanthai 0E4F
+forall 2200
+four 0034
+fourarabic 0664
+fourbengali 09EA
+fourcircle 2463
+fourcircleinversesansserif 278D
+fourdeva 096A
+fourgujarati 0AEA
+fourgurmukhi 0A6A
+fourhackarabic 0664
+fourhangzhou 3024
+fourideographicparen 3223
+fourinferior 2084
+fourmonospace FF14
+fournumeratorbengali 09F7
+fouroldstyle F734
+fourparen 2477
+fourperiod 248B
+fourpersian 06F4
+fourroman 2173
+foursuperior 2074
+fourteencircle 246D
+fourteenparen 2481
+fourteenperiod 2495
+fourthai 0E54
+fourthtonechinese 02CB
+fparen 24A1
+fraction 2044
+franc 20A3
+g 0067
+gabengali 0997
+gacute 01F5
+gadeva 0917
+gafarabic 06AF
+gaffinalarabic FB93
+gafinitialarabic FB94
+gafmedialarabic FB95
+gagujarati 0A97
+gagurmukhi 0A17
+gahiragana 304C
+gakatakana 30AC
+gamma 03B3
+gammalatinsmall 0263
+gammasuperior 02E0
+gangiacoptic 03EB
+gbopomofo 310D
+gbreve 011F
+gcaron 01E7
+gcedilla 0123
+gcircle 24D6
+gcircumflex 011D
+gcommaaccent 0123
+gdot 0121
+gdotaccent 0121
+gecyrillic 0433
+gehiragana 3052
+gekatakana 30B2
+geometricallyequal 2251
+gereshaccenthebrew 059C
+gereshhebrew 05F3
+gereshmuqdamhebrew 059D
+germandbls 00DF
+gershayimaccenthebrew 059E
+gershayimhebrew 05F4
+getamark 3013
+ghabengali 0998
+ghadarmenian 0572
+ghadeva 0918
+ghagujarati 0A98
+ghagurmukhi 0A18
+ghainarabic 063A
+ghainfinalarabic FECE
+ghaininitialarabic FECF
+ghainmedialarabic FED0
+ghemiddlehookcyrillic 0495
+ghestrokecyrillic 0493
+gheupturncyrillic 0491
+ghhadeva 095A
+ghhagurmukhi 0A5A
+ghook 0260
+ghzsquare 3393
+gihiragana 304E
+gikatakana 30AE
+gimarmenian 0563
+gimel 05D2
+gimeldagesh FB32
+gimeldageshhebrew FB32
+gimelhebrew 05D2
+gjecyrillic 0453
+glottalinvertedstroke 01BE
+glottalstop 0294
+glottalstopinverted 0296
+glottalstopmod 02C0
+glottalstopreversed 0295
+glottalstopreversedmod 02C1
+glottalstopreversedsuperior 02E4
+glottalstopstroke 02A1
+glottalstopstrokereversed 02A2
+gmacron 1E21
+gmonospace FF47
+gohiragana 3054
+gokatakana 30B4
+gparen 24A2
+gpasquare 33AC
+gradient 2207
+grave 0060
+gravebelowcmb 0316
+gravecmb 0300
+gravecomb 0300
+gravedeva 0953
+gravelowmod 02CE
+gravemonospace FF40
+gravetonecmb 0340
+greater 003E
+greaterequal 2265
+greaterequalorless 22DB
+greatermonospace FF1E
+greaterorequivalent 2273
+greaterorless 2277
+greateroverequal 2267
+greatersmall FE65
+gscript 0261
+gstroke 01E5
+guhiragana 3050
+guillemotleft 00AB
+guillemotright 00BB
+guilsinglleft 2039
+guilsinglright 203A
+gukatakana 30B0
+guramusquare 3318
+gysquare 33C9
+h 0068
+haabkhasiancyrillic 04A9
+haaltonearabic 06C1
+habengali 09B9
+hadescendercyrillic 04B3
+hadeva 0939
+hagujarati 0AB9
+hagurmukhi 0A39
+haharabic 062D
+hahfinalarabic FEA2
+hahinitialarabic FEA3
+hahiragana 306F
+hahmedialarabic FEA4
+haitusquare 332A
+hakatakana 30CF
+hakatakanahalfwidth FF8A
+halantgurmukhi 0A4D
+hamzaarabic 0621
+hamzadammaarabic 0621 064F
+hamzadammatanarabic 0621 064C
+hamzafathaarabic 0621 064E
+hamzafathatanarabic 0621 064B
+hamzalowarabic 0621
+hamzalowkasraarabic 0621 0650
+hamzalowkasratanarabic 0621 064D
+hamzasukunarabic 0621 0652
+hangulfiller 3164
+hardsigncyrillic 044A
+harpoonleftbarbup 21BC
+harpoonrightbarbup 21C0
+hasquare 33CA
+hatafpatah 05B2
+hatafpatah16 05B2
+hatafpatah23 05B2
+hatafpatah2f 05B2
+hatafpatahhebrew 05B2
+hatafpatahnarrowhebrew 05B2
+hatafpatahquarterhebrew 05B2
+hatafpatahwidehebrew 05B2
+hatafqamats 05B3
+hatafqamats1b 05B3
+hatafqamats28 05B3
+hatafqamats34 05B3
+hatafqamatshebrew 05B3
+hatafqamatsnarrowhebrew 05B3
+hatafqamatsquarterhebrew 05B3
+hatafqamatswidehebrew 05B3
+hatafsegol 05B1
+hatafsegol17 05B1
+hatafsegol24 05B1
+hatafsegol30 05B1
+hatafsegolhebrew 05B1
+hatafsegolnarrowhebrew 05B1
+hatafsegolquarterhebrew 05B1
+hatafsegolwidehebrew 05B1
+hbar 0127
+hbopomofo 310F
+hbrevebelow 1E2B
+hcedilla 1E29
+hcircle 24D7
+hcircumflex 0125
+hdieresis 1E27
+hdotaccent 1E23
+hdotbelow 1E25
+he 05D4
+heart 2665
+heartsuitblack 2665
+heartsuitwhite 2661
+hedagesh FB34
+hedageshhebrew FB34
+hehaltonearabic 06C1
+heharabic 0647
+hehebrew 05D4
+hehfinalaltonearabic FBA7
+hehfinalalttwoarabic FEEA
+hehfinalarabic FEEA
+hehhamzaabovefinalarabic FBA5
+hehhamzaaboveisolatedarabic FBA4
+hehinitialaltonearabic FBA8
+hehinitialarabic FEEB
+hehiragana 3078
+hehmedialaltonearabic FBA9
+hehmedialarabic FEEC
+heiseierasquare 337B
+hekatakana 30D8
+hekatakanahalfwidth FF8D
+hekutaarusquare 3336
+henghook 0267
+herutusquare 3339
+het 05D7
+hethebrew 05D7
+hhook 0266
+hhooksuperior 02B1
+hieuhacirclekorean 327B
+hieuhaparenkorean 321B
+hieuhcirclekorean 326D
+hieuhkorean 314E
+hieuhparenkorean 320D
+hihiragana 3072
+hikatakana 30D2
+hikatakanahalfwidth FF8B
+hiriq 05B4
+hiriq14 05B4
+hiriq21 05B4
+hiriq2d 05B4
+hiriqhebrew 05B4
+hiriqnarrowhebrew 05B4
+hiriqquarterhebrew 05B4
+hiriqwidehebrew 05B4
+hlinebelow 1E96
+hmonospace FF48
+hoarmenian 0570
+hohipthai 0E2B
+hohiragana 307B
+hokatakana 30DB
+hokatakanahalfwidth FF8E
+holam 05B9
+holam19 05B9
+holam26 05B9
+holam32 05B9
+holamhebrew 05B9
+holamnarrowhebrew 05B9
+holamquarterhebrew 05B9
+holamwidehebrew 05B9
+honokhukthai 0E2E
+hookabovecomb 0309
+hookcmb 0309
+hookpalatalizedbelowcmb 0321
+hookretroflexbelowcmb 0322
+hoonsquare 3342
+horicoptic 03E9
+horizontalbar 2015
+horncmb 031B
+hotsprings 2668
+house 2302
+hparen 24A3
+hsuperior 02B0
+hturned 0265
+huhiragana 3075
+huiitosquare 3333
+hukatakana 30D5
+hukatakanahalfwidth FF8C
+hungarumlaut 02DD
+hungarumlautcmb 030B
+hv 0195
+hyphen 002D
+hypheninferior F6E5
+hyphenmonospace FF0D
+hyphensmall FE63
+hyphensuperior F6E6
+hyphentwo 2010
+i 0069
+iacute 00ED
+iacyrillic 044F
+ibengali 0987
+ibopomofo 3127
+ibreve 012D
+icaron 01D0
+icircle 24D8
+icircumflex 00EE
+icyrillic 0456
+idblgrave 0209
+ideographearthcircle 328F
+ideographfirecircle 328B
+ideographicallianceparen 323F
+ideographiccallparen 323A
+ideographiccentrecircle 32A5
+ideographicclose 3006
+ideographiccomma 3001
+ideographiccommaleft FF64
+ideographiccongratulationparen 3237
+ideographiccorrectcircle 32A3
+ideographicearthparen 322F
+ideographicenterpriseparen 323D
+ideographicexcellentcircle 329D
+ideographicfestivalparen 3240
+ideographicfinancialcircle 3296
+ideographicfinancialparen 3236
+ideographicfireparen 322B
+ideographichaveparen 3232
+ideographichighcircle 32A4
+ideographiciterationmark 3005
+ideographiclaborcircle 3298
+ideographiclaborparen 3238
+ideographicleftcircle 32A7
+ideographiclowcircle 32A6
+ideographicmedicinecircle 32A9
+ideographicmetalparen 322E
+ideographicmoonparen 322A
+ideographicnameparen 3234
+ideographicperiod 3002
+ideographicprintcircle 329E
+ideographicreachparen 3243
+ideographicrepresentparen 3239
+ideographicresourceparen 323E
+ideographicrightcircle 32A8
+ideographicsecretcircle 3299
+ideographicselfparen 3242
+ideographicsocietyparen 3233
+ideographicspace 3000
+ideographicspecialparen 3235
+ideographicstockparen 3231
+ideographicstudyparen 323B
+ideographicsunparen 3230
+ideographicsuperviseparen 323C
+ideographicwaterparen 322C
+ideographicwoodparen 322D
+ideographiczero 3007
+ideographmetalcircle 328E
+ideographmooncircle 328A
+ideographnamecircle 3294
+ideographsuncircle 3290
+ideographwatercircle 328C
+ideographwoodcircle 328D
+ideva 0907
+idieresis 00EF
+idieresisacute 1E2F
+idieresiscyrillic 04E5
+idotbelow 1ECB
+iebrevecyrillic 04D7
+iecyrillic 0435
+ieungacirclekorean 3275
+ieungaparenkorean 3215
+ieungcirclekorean 3267
+ieungkorean 3147
+ieungparenkorean 3207
+igrave 00EC
+igujarati 0A87
+igurmukhi 0A07
+ihiragana 3044
+ihookabove 1EC9
+iibengali 0988
+iicyrillic 0438
+iideva 0908
+iigujarati 0A88
+iigurmukhi 0A08
+iimatragurmukhi 0A40
+iinvertedbreve 020B
+iishortcyrillic 0439
+iivowelsignbengali 09C0
+iivowelsigndeva 0940
+iivowelsigngujarati 0AC0
+ij 0133
+ikatakana 30A4
+ikatakanahalfwidth FF72
+ikorean 3163
+ilde 02DC
+iluyhebrew 05AC
+imacron 012B
+imacroncyrillic 04E3
+imageorapproximatelyequal 2253
+imatragurmukhi 0A3F
+imonospace FF49
+increment 2206
+infinity 221E
+iniarmenian 056B
+integral 222B
+integralbottom 2321
+integralbt 2321
+integralex F8F5
+integraltop 2320
+integraltp 2320
+intersection 2229
+intisquare 3305
+invbullet 25D8
+invcircle 25D9
+invsmileface 263B
+iocyrillic 0451
+iogonek 012F
+iota 03B9
+iotadieresis 03CA
+iotadieresistonos 0390
+iotalatin 0269
+iotatonos 03AF
+iparen 24A4
+irigurmukhi 0A72
+ismallhiragana 3043
+ismallkatakana 30A3
+ismallkatakanahalfwidth FF68
+issharbengali 09FA
+istroke 0268
+isuperior F6ED
+iterationhiragana 309D
+iterationkatakana 30FD
+itilde 0129
+itildebelow 1E2D
+iubopomofo 3129
+iucyrillic 044E
+ivowelsignbengali 09BF
+ivowelsigndeva 093F
+ivowelsigngujarati 0ABF
+izhitsacyrillic 0475
+izhitsadblgravecyrillic 0477
+j 006A
+jaarmenian 0571
+jabengali 099C
+jadeva 091C
+jagujarati 0A9C
+jagurmukhi 0A1C
+jbopomofo 3110
+jcaron 01F0
+jcircle 24D9
+jcircumflex 0135
+jcrossedtail 029D
+jdotlessstroke 025F
+jecyrillic 0458
+jeemarabic 062C
+jeemfinalarabic FE9E
+jeeminitialarabic FE9F
+jeemmedialarabic FEA0
+jeharabic 0698
+jehfinalarabic FB8B
+jhabengali 099D
+jhadeva 091D
+jhagujarati 0A9D
+jhagurmukhi 0A1D
+jheharmenian 057B
+jis 3004
+jmonospace FF4A
+jparen 24A5
+jsuperior 02B2
+k 006B
+kabashkircyrillic 04A1
+kabengali 0995
+kacute 1E31
+kacyrillic 043A
+kadescendercyrillic 049B
+kadeva 0915
+kaf 05DB
+kafarabic 0643
+kafdagesh FB3B
+kafdageshhebrew FB3B
+kaffinalarabic FEDA
+kafhebrew 05DB
+kafinitialarabic FEDB
+kafmedialarabic FEDC
+kafrafehebrew FB4D
+kagujarati 0A95
+kagurmukhi 0A15
+kahiragana 304B
+kahookcyrillic 04C4
+kakatakana 30AB
+kakatakanahalfwidth FF76
+kappa 03BA
+kappasymbolgreek 03F0
+kapyeounmieumkorean 3171
+kapyeounphieuphkorean 3184
+kapyeounpieupkorean 3178
+kapyeounssangpieupkorean 3179
+karoriisquare 330D
+kashidaautoarabic 0640
+kashidaautonosidebearingarabic 0640
+kasmallkatakana 30F5
+kasquare 3384
+kasraarabic 0650
+kasratanarabic 064D
+kastrokecyrillic 049F
+katahiraprolongmarkhalfwidth FF70
+kaverticalstrokecyrillic 049D
+kbopomofo 310E
+kcalsquare 3389
+kcaron 01E9
+kcedilla 0137
+kcircle 24DA
+kcommaaccent 0137
+kdotbelow 1E33
+keharmenian 0584
+kehiragana 3051
+kekatakana 30B1
+kekatakanahalfwidth FF79
+kenarmenian 056F
+kesmallkatakana 30F6
+kgreenlandic 0138
+khabengali 0996
+khacyrillic 0445
+khadeva 0916
+khagujarati 0A96
+khagurmukhi 0A16
+khaharabic 062E
+khahfinalarabic FEA6
+khahinitialarabic FEA7
+khahmedialarabic FEA8
+kheicoptic 03E7
+khhadeva 0959
+khhagurmukhi 0A59
+khieukhacirclekorean 3278
+khieukhaparenkorean 3218
+khieukhcirclekorean 326A
+khieukhkorean 314B
+khieukhparenkorean 320A
+khokhaithai 0E02
+khokhonthai 0E05
+khokhuatthai 0E03
+khokhwaithai 0E04
+khomutthai 0E5B
+khook 0199
+khorakhangthai 0E06
+khzsquare 3391
+kihiragana 304D
+kikatakana 30AD
+kikatakanahalfwidth FF77
+kiroguramusquare 3315
+kiromeetorusquare 3316
+kirosquare 3314
+kiyeokacirclekorean 326E
+kiyeokaparenkorean 320E
+kiyeokcirclekorean 3260
+kiyeokkorean 3131
+kiyeokparenkorean 3200
+kiyeoksioskorean 3133
+kjecyrillic 045C
+klinebelow 1E35
+klsquare 3398
+kmcubedsquare 33A6
+kmonospace FF4B
+kmsquaredsquare 33A2
+kohiragana 3053
+kohmsquare 33C0
+kokaithai 0E01
+kokatakana 30B3
+kokatakanahalfwidth FF7A
+kooposquare 331E
+koppacyrillic 0481
+koreanstandardsymbol 327F
+koroniscmb 0343
+kparen 24A6
+kpasquare 33AA
+ksicyrillic 046F
+ktsquare 33CF
+kturned 029E
+kuhiragana 304F
+kukatakana 30AF
+kukatakanahalfwidth FF78
+kvsquare 33B8
+kwsquare 33BE
+l 006C
+labengali 09B2
+lacute 013A
+ladeva 0932
+lagujarati 0AB2
+lagurmukhi 0A32
+lakkhangyaothai 0E45
+lamaleffinalarabic FEFC
+lamalefhamzaabovefinalarabic FEF8
+lamalefhamzaaboveisolatedarabic FEF7
+lamalefhamzabelowfinalarabic FEFA
+lamalefhamzabelowisolatedarabic FEF9
+lamalefisolatedarabic FEFB
+lamalefmaddaabovefinalarabic FEF6
+lamalefmaddaaboveisolatedarabic FEF5
+lamarabic 0644
+lambda 03BB
+lambdastroke 019B
+lamed 05DC
+lameddagesh FB3C
+lameddageshhebrew FB3C
+lamedhebrew 05DC
+lamedholam 05DC 05B9
+lamedholamdagesh 05DC 05B9 05BC
+lamedholamdageshhebrew 05DC 05B9 05BC
+lamedholamhebrew 05DC 05B9
+lamfinalarabic FEDE
+lamhahinitialarabic FCCA
+laminitialarabic FEDF
+lamjeeminitialarabic FCC9
+lamkhahinitialarabic FCCB
+lamlamhehisolatedarabic FDF2
+lammedialarabic FEE0
+lammeemhahinitialarabic FD88
+lammeeminitialarabic FCCC
+lammeemjeeminitialarabic FEDF FEE4 FEA0
+lammeemkhahinitialarabic FEDF FEE4 FEA8
+largecircle 25EF
+lbar 019A
+lbelt 026C
+lbopomofo 310C
+lcaron 013E
+lcedilla 013C
+lcircle 24DB
+lcircumflexbelow 1E3D
+lcommaaccent 013C
+ldot 0140
+ldotaccent 0140
+ldotbelow 1E37
+ldotbelowmacron 1E39
+leftangleabovecmb 031A
+lefttackbelowcmb 0318
+less 003C
+lessequal 2264
+lessequalorgreater 22DA
+lessmonospace FF1C
+lessorequivalent 2272
+lessorgreater 2276
+lessoverequal 2266
+lesssmall FE64
+lezh 026E
+lfblock 258C
+lhookretroflex 026D
+lira 20A4
+liwnarmenian 056C
+lj 01C9
+ljecyrillic 0459
+ll F6C0
+lladeva 0933
+llagujarati 0AB3
+llinebelow 1E3B
+llladeva 0934
+llvocalicbengali 09E1
+llvocalicdeva 0961
+llvocalicvowelsignbengali 09E3
+llvocalicvowelsigndeva 0963
+lmiddletilde 026B
+lmonospace FF4C
+lmsquare 33D0
+lochulathai 0E2C
+logicaland 2227
+logicalnot 00AC
+logicalnotreversed 2310
+logicalor 2228
+lolingthai 0E25
+longs 017F
+lowlinecenterline FE4E
+lowlinecmb 0332
+lowlinedashed FE4D
+lozenge 25CA
+lparen 24A7
+lslash 0142
+lsquare 2113
+lsuperior F6EE
+ltshade 2591
+luthai 0E26
+lvocalicbengali 098C
+lvocalicdeva 090C
+lvocalicvowelsignbengali 09E2
+lvocalicvowelsigndeva 0962
+lxsquare 33D3
+m 006D
+mabengali 09AE
+macron 00AF
+macronbelowcmb 0331
+macroncmb 0304
+macronlowmod 02CD
+macronmonospace FFE3
+macute 1E3F
+madeva 092E
+magujarati 0AAE
+magurmukhi 0A2E
+mahapakhhebrew 05A4
+mahapakhlefthebrew 05A4
+mahiragana 307E
+maichattawalowleftthai F895
+maichattawalowrightthai F894
+maichattawathai 0E4B
+maichattawaupperleftthai F893
+maieklowleftthai F88C
+maieklowrightthai F88B
+maiekthai 0E48
+maiekupperleftthai F88A
+maihanakatleftthai F884
+maihanakatthai 0E31
+maitaikhuleftthai F889
+maitaikhuthai 0E47
+maitholowleftthai F88F
+maitholowrightthai F88E
+maithothai 0E49
+maithoupperleftthai F88D
+maitrilowleftthai F892
+maitrilowrightthai F891
+maitrithai 0E4A
+maitriupperleftthai F890
+maiyamokthai 0E46
+makatakana 30DE
+makatakanahalfwidth FF8F
+male 2642
+mansyonsquare 3347
+maqafhebrew 05BE
+mars 2642
+masoracirclehebrew 05AF
+masquare 3383
+mbopomofo 3107
+mbsquare 33D4
+mcircle 24DC
+mcubedsquare 33A5
+mdotaccent 1E41
+mdotbelow 1E43
+meemarabic 0645
+meemfinalarabic FEE2
+meeminitialarabic FEE3
+meemmedialarabic FEE4
+meemmeeminitialarabic FCD1
+meemmeemisolatedarabic FC48
+meetorusquare 334D
+mehiragana 3081
+meizierasquare 337E
+mekatakana 30E1
+mekatakanahalfwidth FF92
+mem 05DE
+memdagesh FB3E
+memdageshhebrew FB3E
+memhebrew 05DE
+menarmenian 0574
+merkhahebrew 05A5
+merkhakefulahebrew 05A6
+merkhakefulalefthebrew 05A6
+merkhalefthebrew 05A5
+mhook 0271
+mhzsquare 3392
+middledotkatakanahalfwidth FF65
+middot 00B7
+mieumacirclekorean 3272
+mieumaparenkorean 3212
+mieumcirclekorean 3264
+mieumkorean 3141
+mieumpansioskorean 3170
+mieumparenkorean 3204
+mieumpieupkorean 316E
+mieumsioskorean 316F
+mihiragana 307F
+mikatakana 30DF
+mikatakanahalfwidth FF90
+minus 2212
+minusbelowcmb 0320
+minuscircle 2296
+minusmod 02D7
+minusplus 2213
+minute 2032
+miribaarusquare 334A
+mirisquare 3349
+mlonglegturned 0270
+mlsquare 3396
+mmcubedsquare 33A3
+mmonospace FF4D
+mmsquaredsquare 339F
+mohiragana 3082
+mohmsquare 33C1
+mokatakana 30E2
+mokatakanahalfwidth FF93
+molsquare 33D6
+momathai 0E21
+moverssquare 33A7
+moverssquaredsquare 33A8
+mparen 24A8
+mpasquare 33AB
+mssquare 33B3
+msuperior F6EF
+mturned 026F
+mu 00B5
+mu1 00B5
+muasquare 3382
+muchgreater 226B
+muchless 226A
+mufsquare 338C
+mugreek 03BC
+mugsquare 338D
+muhiragana 3080
+mukatakana 30E0
+mukatakanahalfwidth FF91
+mulsquare 3395
+multiply 00D7
+mumsquare 339B
+munahhebrew 05A3
+munahlefthebrew 05A3
+musicalnote 266A
+musicalnotedbl 266B
+musicflatsign 266D
+musicsharpsign 266F
+mussquare 33B2
+muvsquare 33B6
+muwsquare 33BC
+mvmegasquare 33B9
+mvsquare 33B7
+mwmegasquare 33BF
+mwsquare 33BD
+n 006E
+nabengali 09A8
+nabla 2207
+nacute 0144
+nadeva 0928
+nagujarati 0AA8
+nagurmukhi 0A28
+nahiragana 306A
+nakatakana 30CA
+nakatakanahalfwidth FF85
+napostrophe 0149
+nasquare 3381
+nbopomofo 310B
+nbspace 00A0
+ncaron 0148
+ncedilla 0146
+ncircle 24DD
+ncircumflexbelow 1E4B
+ncommaaccent 0146
+ndotaccent 1E45
+ndotbelow 1E47
+nehiragana 306D
+nekatakana 30CD
+nekatakanahalfwidth FF88
+newsheqelsign 20AA
+nfsquare 338B
+ngabengali 0999
+ngadeva 0919
+ngagujarati 0A99
+ngagurmukhi 0A19
+ngonguthai 0E07
+nhiragana 3093
+nhookleft 0272
+nhookretroflex 0273
+nieunacirclekorean 326F
+nieunaparenkorean 320F
+nieuncieuckorean 3135
+nieuncirclekorean 3261
+nieunhieuhkorean 3136
+nieunkorean 3134
+nieunpansioskorean 3168
+nieunparenkorean 3201
+nieunsioskorean 3167
+nieuntikeutkorean 3166
+nihiragana 306B
+nikatakana 30CB
+nikatakanahalfwidth FF86
+nikhahitleftthai F899
+nikhahitthai 0E4D
+nine 0039
+ninearabic 0669
+ninebengali 09EF
+ninecircle 2468
+ninecircleinversesansserif 2792
+ninedeva 096F
+ninegujarati 0AEF
+ninegurmukhi 0A6F
+ninehackarabic 0669
+ninehangzhou 3029
+nineideographicparen 3228
+nineinferior 2089
+ninemonospace FF19
+nineoldstyle F739
+nineparen 247C
+nineperiod 2490
+ninepersian 06F9
+nineroman 2178
+ninesuperior 2079
+nineteencircle 2472
+nineteenparen 2486
+nineteenperiod 249A
+ninethai 0E59
+nj 01CC
+njecyrillic 045A
+nkatakana 30F3
+nkatakanahalfwidth FF9D
+nlegrightlong 019E
+nlinebelow 1E49
+nmonospace FF4E
+nmsquare 339A
+nnabengali 09A3
+nnadeva 0923
+nnagujarati 0AA3
+nnagurmukhi 0A23
+nnnadeva 0929
+nohiragana 306E
+nokatakana 30CE
+nokatakanahalfwidth FF89
+nonbreakingspace 00A0
+nonenthai 0E13
+nonuthai 0E19
+noonarabic 0646
+noonfinalarabic FEE6
+noonghunnaarabic 06BA
+noonghunnafinalarabic FB9F
+noonhehinitialarabic FEE7 FEEC
+nooninitialarabic FEE7
+noonjeeminitialarabic FCD2
+noonjeemisolatedarabic FC4B
+noonmedialarabic FEE8
+noonmeeminitialarabic FCD5
+noonmeemisolatedarabic FC4E
+noonnoonfinalarabic FC8D
+notcontains 220C
+notelement 2209
+notelementof 2209
+notequal 2260
+notgreater 226F
+notgreaternorequal 2271
+notgreaternorless 2279
+notidentical 2262
+notless 226E
+notlessnorequal 2270
+notparallel 2226
+notprecedes 2280
+notsubset 2284
+notsucceeds 2281
+notsuperset 2285
+nowarmenian 0576
+nparen 24A9
+nssquare 33B1
+nsuperior 207F
+ntilde 00F1
+nu 03BD
+nuhiragana 306C
+nukatakana 30CC
+nukatakanahalfwidth FF87
+nuktabengali 09BC
+nuktadeva 093C
+nuktagujarati 0ABC
+nuktagurmukhi 0A3C
+numbersign 0023
+numbersignmonospace FF03
+numbersignsmall FE5F
+numeralsigngreek 0374
+numeralsignlowergreek 0375
+numero 2116
+nun 05E0
+nundagesh FB40
+nundageshhebrew FB40
+nunhebrew 05E0
+nvsquare 33B5
+nwsquare 33BB
+nyabengali 099E
+nyadeva 091E
+nyagujarati 0A9E
+nyagurmukhi 0A1E
+o 006F
+oacute 00F3
+oangthai 0E2D
+obarred 0275
+obarredcyrillic 04E9
+obarreddieresiscyrillic 04EB
+obengali 0993
+obopomofo 311B
+obreve 014F
+ocandradeva 0911
+ocandragujarati 0A91
+ocandravowelsigndeva 0949
+ocandravowelsigngujarati 0AC9
+ocaron 01D2
+ocircle 24DE
+ocircumflex 00F4
+ocircumflexacute 1ED1
+ocircumflexdotbelow 1ED9
+ocircumflexgrave 1ED3
+ocircumflexhookabove 1ED5
+ocircumflextilde 1ED7
+ocyrillic 043E
+odblacute 0151
+odblgrave 020D
+odeva 0913
+odieresis 00F6
+odieresiscyrillic 04E7
+odotbelow 1ECD
+oe 0153
+oekorean 315A
+ogonek 02DB
+ogonekcmb 0328
+ograve 00F2
+ogujarati 0A93
+oharmenian 0585
+ohiragana 304A
+ohookabove 1ECF
+ohorn 01A1
+ohornacute 1EDB
+ohorndotbelow 1EE3
+ohorngrave 1EDD
+ohornhookabove 1EDF
+ohorntilde 1EE1
+ohungarumlaut 0151
+oi 01A3
+oinvertedbreve 020F
+okatakana 30AA
+okatakanahalfwidth FF75
+okorean 3157
+olehebrew 05AB
+omacron 014D
+omacronacute 1E53
+omacrongrave 1E51
+omdeva 0950
+omega 03C9
+omega1 03D6
+omegacyrillic 0461
+omegalatinclosed 0277
+omegaroundcyrillic 047B
+omegatitlocyrillic 047D
+omegatonos 03CE
+omgujarati 0AD0
+omicron 03BF
+omicrontonos 03CC
+omonospace FF4F
+one 0031
+onearabic 0661
+onebengali 09E7
+onecircle 2460
+onecircleinversesansserif 278A
+onedeva 0967
+onedotenleader 2024
+oneeighth 215B
+onefitted F6DC
+onegujarati 0AE7
+onegurmukhi 0A67
+onehackarabic 0661
+onehalf 00BD
+onehangzhou 3021
+oneideographicparen 3220
+oneinferior 2081
+onemonospace FF11
+onenumeratorbengali 09F4
+oneoldstyle F731
+oneparen 2474
+oneperiod 2488
+onepersian 06F1
+onequarter 00BC
+oneroman 2170
+onesuperior 00B9
+onethai 0E51
+onethird 2153
+oogonek 01EB
+oogonekmacron 01ED
+oogurmukhi 0A13
+oomatragurmukhi 0A4B
+oopen 0254
+oparen 24AA
+openbullet 25E6
+option 2325
+ordfeminine 00AA
+ordmasculine 00BA
+orthogonal 221F
+oshortdeva 0912
+oshortvowelsigndeva 094A
+oslash 00F8
+oslashacute 01FF
+osmallhiragana 3049
+osmallkatakana 30A9
+osmallkatakanahalfwidth FF6B
+ostrokeacute 01FF
+osuperior F6F0
+otcyrillic 047F
+otilde 00F5
+otildeacute 1E4D
+otildedieresis 1E4F
+oubopomofo 3121
+overline 203E
+overlinecenterline FE4A
+overlinecmb 0305
+overlinedashed FE49
+overlinedblwavy FE4C
+overlinewavy FE4B
+overscore 00AF
+ovowelsignbengali 09CB
+ovowelsigndeva 094B
+ovowelsigngujarati 0ACB
+p 0070
+paampssquare 3380
+paasentosquare 332B
+pabengali 09AA
+pacute 1E55
+padeva 092A
+pagedown 21DF
+pageup 21DE
+pagujarati 0AAA
+pagurmukhi 0A2A
+pahiragana 3071
+paiyannoithai 0E2F
+pakatakana 30D1
+palatalizationcyrilliccmb 0484
+palochkacyrillic 04C0
+pansioskorean 317F
+paragraph 00B6
+parallel 2225
+parenleft 0028
+parenleftaltonearabic FD3E
+parenleftbt F8ED
+parenleftex F8EC
+parenleftinferior 208D
+parenleftmonospace FF08
+parenleftsmall FE59
+parenleftsuperior 207D
+parenlefttp F8EB
+parenleftvertical FE35
+parenright 0029
+parenrightaltonearabic FD3F
+parenrightbt F8F8
+parenrightex F8F7
+parenrightinferior 208E
+parenrightmonospace FF09
+parenrightsmall FE5A
+parenrightsuperior 207E
+parenrighttp F8F6
+parenrightvertical FE36
+partialdiff 2202
+paseqhebrew 05C0
+pashtahebrew 0599
+pasquare 33A9
+patah 05B7
+patah11 05B7
+patah1d 05B7
+patah2a 05B7
+patahhebrew 05B7
+patahnarrowhebrew 05B7
+patahquarterhebrew 05B7
+patahwidehebrew 05B7
+pazerhebrew 05A1
+pbopomofo 3106
+pcircle 24DF
+pdotaccent 1E57
+pe 05E4
+pecyrillic 043F
+pedagesh FB44
+pedageshhebrew FB44
+peezisquare 333B
+pefinaldageshhebrew FB43
+peharabic 067E
+peharmenian 057A
+pehebrew 05E4
+pehfinalarabic FB57
+pehinitialarabic FB58
+pehiragana 307A
+pehmedialarabic FB59
+pekatakana 30DA
+pemiddlehookcyrillic 04A7
+perafehebrew FB4E
+percent 0025
+percentarabic 066A
+percentmonospace FF05
+percentsmall FE6A
+period 002E
+periodarmenian 0589
+periodcentered 00B7
+periodhalfwidth FF61
+periodinferior F6E7
+periodmonospace FF0E
+periodsmall FE52
+periodsuperior F6E8
+perispomenigreekcmb 0342
+perpendicular 22A5
+perthousand 2030
+peseta 20A7
+pfsquare 338A
+phabengali 09AB
+phadeva 092B
+phagujarati 0AAB
+phagurmukhi 0A2B
+phi 03C6
+phi1 03D5
+phieuphacirclekorean 327A
+phieuphaparenkorean 321A
+phieuphcirclekorean 326C
+phieuphkorean 314D
+phieuphparenkorean 320C
+philatin 0278
+phinthuthai 0E3A
+phisymbolgreek 03D5
+phook 01A5
+phophanthai 0E1E
+phophungthai 0E1C
+phosamphaothai 0E20
+pi 03C0
+pieupacirclekorean 3273
+pieupaparenkorean 3213
+pieupcieuckorean 3176
+pieupcirclekorean 3265
+pieupkiyeokkorean 3172
+pieupkorean 3142
+pieupparenkorean 3205
+pieupsioskiyeokkorean 3174
+pieupsioskorean 3144
+pieupsiostikeutkorean 3175
+pieupthieuthkorean 3177
+pieuptikeutkorean 3173
+pihiragana 3074
+pikatakana 30D4
+pisymbolgreek 03D6
+piwrarmenian 0583
+plus 002B
+plusbelowcmb 031F
+pluscircle 2295
+plusminus 00B1
+plusmod 02D6
+plusmonospace FF0B
+plussmall FE62
+plussuperior 207A
+pmonospace FF50
+pmsquare 33D8
+pohiragana 307D
+pointingindexdownwhite 261F
+pointingindexleftwhite 261C
+pointingindexrightwhite 261E
+pointingindexupwhite 261D
+pokatakana 30DD
+poplathai 0E1B
+postalmark 3012
+postalmarkface 3020
+pparen 24AB
+precedes 227A
+prescription 211E
+primemod 02B9
+primereversed 2035
+product 220F
+projective 2305
+prolongedkana 30FC
+propellor 2318
+propersubset 2282
+propersuperset 2283
+proportion 2237
+proportional 221D
+psi 03C8
+psicyrillic 0471
+psilipneumatacyrilliccmb 0486
+pssquare 33B0
+puhiragana 3077
+pukatakana 30D7
+pvsquare 33B4
+pwsquare 33BA
+q 0071
+qadeva 0958
+qadmahebrew 05A8
+qafarabic 0642
+qaffinalarabic FED6
+qafinitialarabic FED7
+qafmedialarabic FED8
+qamats 05B8
+qamats10 05B8
+qamats1a 05B8
+qamats1c 05B8
+qamats27 05B8
+qamats29 05B8
+qamats33 05B8
+qamatsde 05B8
+qamatshebrew 05B8
+qamatsnarrowhebrew 05B8
+qamatsqatanhebrew 05B8
+qamatsqatannarrowhebrew 05B8
+qamatsqatanquarterhebrew 05B8
+qamatsqatanwidehebrew 05B8
+qamatsquarterhebrew 05B8
+qamatswidehebrew 05B8
+qarneyparahebrew 059F
+qbopomofo 3111
+qcircle 24E0
+qhook 02A0
+qmonospace FF51
+qof 05E7
+qofdagesh FB47
+qofdageshhebrew FB47
+qofhatafpatah 05E7 05B2
+qofhatafpatahhebrew 05E7 05B2
+qofhatafsegol 05E7 05B1
+qofhatafsegolhebrew 05E7 05B1
+qofhebrew 05E7
+qofhiriq 05E7 05B4
+qofhiriqhebrew 05E7 05B4
+qofholam 05E7 05B9
+qofholamhebrew 05E7 05B9
+qofpatah 05E7 05B7
+qofpatahhebrew 05E7 05B7
+qofqamats 05E7 05B8
+qofqamatshebrew 05E7 05B8
+qofqubuts 05E7 05BB
+qofqubutshebrew 05E7 05BB
+qofsegol 05E7 05B6
+qofsegolhebrew 05E7 05B6
+qofsheva 05E7 05B0
+qofshevahebrew 05E7 05B0
+qoftsere 05E7 05B5
+qoftserehebrew 05E7 05B5
+qparen 24AC
+quarternote 2669
+qubuts 05BB
+qubuts18 05BB
+qubuts25 05BB
+qubuts31 05BB
+qubutshebrew 05BB
+qubutsnarrowhebrew 05BB
+qubutsquarterhebrew 05BB
+qubutswidehebrew 05BB
+question 003F
+questionarabic 061F
+questionarmenian 055E
+questiondown 00BF
+questiondownsmall F7BF
+questiongreek 037E
+questionmonospace FF1F
+questionsmall F73F
+quotedbl 0022
+quotedblbase 201E
+quotedblleft 201C
+quotedblmonospace FF02
+quotedblprime 301E
+quotedblprimereversed 301D
+quotedblright 201D
+quoteleft 2018
+quoteleftreversed 201B
+quotereversed 201B
+quoteright 2019
+quoterightn 0149
+quotesinglbase 201A
+quotesingle 0027
+quotesinglemonospace FF07
+r 0072
+raarmenian 057C
+rabengali 09B0
+racute 0155
+radeva 0930
+radical 221A
+radicalex F8E5
+radoverssquare 33AE
+radoverssquaredsquare 33AF
+radsquare 33AD
+rafe 05BF
+rafehebrew 05BF
+ragujarati 0AB0
+ragurmukhi 0A30
+rahiragana 3089
+rakatakana 30E9
+rakatakanahalfwidth FF97
+ralowerdiagonalbengali 09F1
+ramiddlediagonalbengali 09F0
+ramshorn 0264
+ratio 2236
+rbopomofo 3116
+rcaron 0159
+rcedilla 0157
+rcircle 24E1
+rcommaaccent 0157
+rdblgrave 0211
+rdotaccent 1E59
+rdotbelow 1E5B
+rdotbelowmacron 1E5D
+referencemark 203B
+reflexsubset 2286
+reflexsuperset 2287
+registered 00AE
+registersans F8E8
+registerserif F6DA
+reharabic 0631
+reharmenian 0580
+rehfinalarabic FEAE
+rehiragana 308C
+rehyehaleflamarabic 0631 FEF3 FE8E 0644
+rekatakana 30EC
+rekatakanahalfwidth FF9A
+resh 05E8
+reshdageshhebrew FB48
+reshhatafpatah 05E8 05B2
+reshhatafpatahhebrew 05E8 05B2
+reshhatafsegol 05E8 05B1
+reshhatafsegolhebrew 05E8 05B1
+reshhebrew 05E8
+reshhiriq 05E8 05B4
+reshhiriqhebrew 05E8 05B4
+reshholam 05E8 05B9
+reshholamhebrew 05E8 05B9
+reshpatah 05E8 05B7
+reshpatahhebrew 05E8 05B7
+reshqamats 05E8 05B8
+reshqamatshebrew 05E8 05B8
+reshqubuts 05E8 05BB
+reshqubutshebrew 05E8 05BB
+reshsegol 05E8 05B6
+reshsegolhebrew 05E8 05B6
+reshsheva 05E8 05B0
+reshshevahebrew 05E8 05B0
+reshtsere 05E8 05B5
+reshtserehebrew 05E8 05B5
+reversedtilde 223D
+reviahebrew 0597
+reviamugrashhebrew 0597
+revlogicalnot 2310
+rfishhook 027E
+rfishhookreversed 027F
+rhabengali 09DD
+rhadeva 095D
+rho 03C1
+rhook 027D
+rhookturned 027B
+rhookturnedsuperior 02B5
+rhosymbolgreek 03F1
+rhotichookmod 02DE
+rieulacirclekorean 3271
+rieulaparenkorean 3211
+rieulcirclekorean 3263
+rieulhieuhkorean 3140
+rieulkiyeokkorean 313A
+rieulkiyeoksioskorean 3169
+rieulkorean 3139
+rieulmieumkorean 313B
+rieulpansioskorean 316C
+rieulparenkorean 3203
+rieulphieuphkorean 313F
+rieulpieupkorean 313C
+rieulpieupsioskorean 316B
+rieulsioskorean 313D
+rieulthieuthkorean 313E
+rieultikeutkorean 316A
+rieulyeorinhieuhkorean 316D
+rightangle 221F
+righttackbelowcmb 0319
+righttriangle 22BF
+rihiragana 308A
+rikatakana 30EA
+rikatakanahalfwidth FF98
+ring 02DA
+ringbelowcmb 0325
+ringcmb 030A
+ringhalfleft 02BF
+ringhalfleftarmenian 0559
+ringhalfleftbelowcmb 031C
+ringhalfleftcentered 02D3
+ringhalfright 02BE
+ringhalfrightbelowcmb 0339
+ringhalfrightcentered 02D2
+rinvertedbreve 0213
+rittorusquare 3351
+rlinebelow 1E5F
+rlongleg 027C
+rlonglegturned 027A
+rmonospace FF52
+rohiragana 308D
+rokatakana 30ED
+rokatakanahalfwidth FF9B
+roruathai 0E23
+rparen 24AD
+rrabengali 09DC
+rradeva 0931
+rragurmukhi 0A5C
+rreharabic 0691
+rrehfinalarabic FB8D
+rrvocalicbengali 09E0
+rrvocalicdeva 0960
+rrvocalicgujarati 0AE0
+rrvocalicvowelsignbengali 09C4
+rrvocalicvowelsigndeva 0944
+rrvocalicvowelsigngujarati 0AC4
+rsuperior F6F1
+rtblock 2590
+rturned 0279
+rturnedsuperior 02B4
+ruhiragana 308B
+rukatakana 30EB
+rukatakanahalfwidth FF99
+rupeemarkbengali 09F2
+rupeesignbengali 09F3
+rupiah F6DD
+ruthai 0E24
+rvocalicbengali 098B
+rvocalicdeva 090B
+rvocalicgujarati 0A8B
+rvocalicvowelsignbengali 09C3
+rvocalicvowelsigndeva 0943
+rvocalicvowelsigngujarati 0AC3
+s 0073
+sabengali 09B8
+sacute 015B
+sacutedotaccent 1E65
+sadarabic 0635
+sadeva 0938
+sadfinalarabic FEBA
+sadinitialarabic FEBB
+sadmedialarabic FEBC
+sagujarati 0AB8
+sagurmukhi 0A38
+sahiragana 3055
+sakatakana 30B5
+sakatakanahalfwidth FF7B
+sallallahoualayhewasallamarabic FDFA
+samekh 05E1
+samekhdagesh FB41
+samekhdageshhebrew FB41
+samekhhebrew 05E1
+saraaathai 0E32
+saraaethai 0E41
+saraaimaimalaithai 0E44
+saraaimaimuanthai 0E43
+saraamthai 0E33
+saraathai 0E30
+saraethai 0E40
+saraiileftthai F886
+saraiithai 0E35
+saraileftthai F885
+saraithai 0E34
+saraothai 0E42
+saraueeleftthai F888
+saraueethai 0E37
+saraueleftthai F887
+sarauethai 0E36
+sarauthai 0E38
+sarauuthai 0E39
+sbopomofo 3119
+scaron 0161
+scarondotaccent 1E67
+scedilla 015F
+schwa 0259
+schwacyrillic 04D9
+schwadieresiscyrillic 04DB
+schwahook 025A
+scircle 24E2
+scircumflex 015D
+scommaaccent 0219
+sdotaccent 1E61
+sdotbelow 1E63
+sdotbelowdotaccent 1E69
+seagullbelowcmb 033C
+second 2033
+secondtonechinese 02CA
+section 00A7
+seenarabic 0633
+seenfinalarabic FEB2
+seeninitialarabic FEB3
+seenmedialarabic FEB4
+segol 05B6
+segol13 05B6
+segol1f 05B6
+segol2c 05B6
+segolhebrew 05B6
+segolnarrowhebrew 05B6
+segolquarterhebrew 05B6
+segoltahebrew 0592
+segolwidehebrew 05B6
+seharmenian 057D
+sehiragana 305B
+sekatakana 30BB
+sekatakanahalfwidth FF7E
+semicolon 003B
+semicolonarabic 061B
+semicolonmonospace FF1B
+semicolonsmall FE54
+semivoicedmarkkana 309C
+semivoicedmarkkanahalfwidth FF9F
+sentisquare 3322
+sentosquare 3323
+seven 0037
+sevenarabic 0667
+sevenbengali 09ED
+sevencircle 2466
+sevencircleinversesansserif 2790
+sevendeva 096D
+seveneighths 215E
+sevengujarati 0AED
+sevengurmukhi 0A6D
+sevenhackarabic 0667
+sevenhangzhou 3027
+sevenideographicparen 3226
+seveninferior 2087
+sevenmonospace FF17
+sevenoldstyle F737
+sevenparen 247A
+sevenperiod 248E
+sevenpersian 06F7
+sevenroman 2176
+sevensuperior 2077
+seventeencircle 2470
+seventeenparen 2484
+seventeenperiod 2498
+seventhai 0E57
+sfthyphen 00AD
+shaarmenian 0577
+shabengali 09B6
+shacyrillic 0448
+shaddaarabic 0651
+shaddadammaarabic FC61
+shaddadammatanarabic FC5E
+shaddafathaarabic FC60
+shaddafathatanarabic 0651 064B
+shaddakasraarabic FC62
+shaddakasratanarabic FC5F
+shade 2592
+shadedark 2593
+shadelight 2591
+shademedium 2592
+shadeva 0936
+shagujarati 0AB6
+shagurmukhi 0A36
+shalshelethebrew 0593
+shbopomofo 3115
+shchacyrillic 0449
+sheenarabic 0634
+sheenfinalarabic FEB6
+sheeninitialarabic FEB7
+sheenmedialarabic FEB8
+sheicoptic 03E3
+sheqel 20AA
+sheqelhebrew 20AA
+sheva 05B0
+sheva115 05B0
+sheva15 05B0
+sheva22 05B0
+sheva2e 05B0
+shevahebrew 05B0
+shevanarrowhebrew 05B0
+shevaquarterhebrew 05B0
+shevawidehebrew 05B0
+shhacyrillic 04BB
+shimacoptic 03ED
+shin 05E9
+shindagesh FB49
+shindageshhebrew FB49
+shindageshshindot FB2C
+shindageshshindothebrew FB2C
+shindageshsindot FB2D
+shindageshsindothebrew FB2D
+shindothebrew 05C1
+shinhebrew 05E9
+shinshindot FB2A
+shinshindothebrew FB2A
+shinsindot FB2B
+shinsindothebrew FB2B
+shook 0282
+sigma 03C3
+sigma1 03C2
+sigmafinal 03C2
+sigmalunatesymbolgreek 03F2
+sihiragana 3057
+sikatakana 30B7
+sikatakanahalfwidth FF7C
+siluqhebrew 05BD
+siluqlefthebrew 05BD
+similar 223C
+sindothebrew 05C2
+siosacirclekorean 3274
+siosaparenkorean 3214
+sioscieuckorean 317E
+sioscirclekorean 3266
+sioskiyeokkorean 317A
+sioskorean 3145
+siosnieunkorean 317B
+siosparenkorean 3206
+siospieupkorean 317D
+siostikeutkorean 317C
+six 0036
+sixarabic 0666
+sixbengali 09EC
+sixcircle 2465
+sixcircleinversesansserif 278F
+sixdeva 096C
+sixgujarati 0AEC
+sixgurmukhi 0A6C
+sixhackarabic 0666
+sixhangzhou 3026
+sixideographicparen 3225
+sixinferior 2086
+sixmonospace FF16
+sixoldstyle F736
+sixparen 2479
+sixperiod 248D
+sixpersian 06F6
+sixroman 2175
+sixsuperior 2076
+sixteencircle 246F
+sixteencurrencydenominatorbengali 09F9
+sixteenparen 2483
+sixteenperiod 2497
+sixthai 0E56
+slash 002F
+slashmonospace FF0F
+slong 017F
+slongdotaccent 1E9B
+smileface 263A
+smonospace FF53
+sofpasuqhebrew 05C3
+softhyphen 00AD
+softsigncyrillic 044C
+sohiragana 305D
+sokatakana 30BD
+sokatakanahalfwidth FF7F
+soliduslongoverlaycmb 0338
+solidusshortoverlaycmb 0337
+sorusithai 0E29
+sosalathai 0E28
+sosothai 0E0B
+sosuathai 0E2A
+space 0020
+spacehackarabic 0020
+spade 2660
+spadesuitblack 2660
+spadesuitwhite 2664
+sparen 24AE
+squarebelowcmb 033B
+squarecc 33C4
+squarecm 339D
+squarediagonalcrosshatchfill 25A9
+squarehorizontalfill 25A4
+squarekg 338F
+squarekm 339E
+squarekmcapital 33CE
+squareln 33D1
+squarelog 33D2
+squaremg 338E
+squaremil 33D5
+squaremm 339C
+squaremsquared 33A1
+squareorthogonalcrosshatchfill 25A6
+squareupperlefttolowerrightfill 25A7
+squareupperrighttolowerleftfill 25A8
+squareverticalfill 25A5
+squarewhitewithsmallblack 25A3
+srsquare 33DB
+ssabengali 09B7
+ssadeva 0937
+ssagujarati 0AB7
+ssangcieuckorean 3149
+ssanghieuhkorean 3185
+ssangieungkorean 3180
+ssangkiyeokkorean 3132
+ssangnieunkorean 3165
+ssangpieupkorean 3143
+ssangsioskorean 3146
+ssangtikeutkorean 3138
+ssuperior F6F2
+sterling 00A3
+sterlingmonospace FFE1
+strokelongoverlaycmb 0336
+strokeshortoverlaycmb 0335
+subset 2282
+subsetnotequal 228A
+subsetorequal 2286
+succeeds 227B
+suchthat 220B
+suhiragana 3059
+sukatakana 30B9
+sukatakanahalfwidth FF7D
+sukunarabic 0652
+summation 2211
+sun 263C
+superset 2283
+supersetnotequal 228B
+supersetorequal 2287
+svsquare 33DC
+syouwaerasquare 337C
+t 0074
+tabengali 09A4
+tackdown 22A4
+tackleft 22A3
+tadeva 0924
+tagujarati 0AA4
+tagurmukhi 0A24
+taharabic 0637
+tahfinalarabic FEC2
+tahinitialarabic FEC3
+tahiragana 305F
+tahmedialarabic FEC4
+taisyouerasquare 337D
+takatakana 30BF
+takatakanahalfwidth FF80
+tatweelarabic 0640
+tau 03C4
+tav 05EA
+tavdages FB4A
+tavdagesh FB4A
+tavdageshhebrew FB4A
+tavhebrew 05EA
+tbar 0167
+tbopomofo 310A
+tcaron 0165
+tccurl 02A8
+tcedilla 0163
+tcheharabic 0686
+tchehfinalarabic FB7B
+tchehinitialarabic FB7C
+tchehmedialarabic FB7D
+tchehmeeminitialarabic FB7C FEE4
+tcircle 24E3
+tcircumflexbelow 1E71
+tcommaaccent 0163
+tdieresis 1E97
+tdotaccent 1E6B
+tdotbelow 1E6D
+tecyrillic 0442
+tedescendercyrillic 04AD
+teharabic 062A
+tehfinalarabic FE96
+tehhahinitialarabic FCA2
+tehhahisolatedarabic FC0C
+tehinitialarabic FE97
+tehiragana 3066
+tehjeeminitialarabic FCA1
+tehjeemisolatedarabic FC0B
+tehmarbutaarabic 0629
+tehmarbutafinalarabic FE94
+tehmedialarabic FE98
+tehmeeminitialarabic FCA4
+tehmeemisolatedarabic FC0E
+tehnoonfinalarabic FC73
+tekatakana 30C6
+tekatakanahalfwidth FF83
+telephone 2121
+telephoneblack 260E
+telishagedolahebrew 05A0
+telishaqetanahebrew 05A9
+tencircle 2469
+tenideographicparen 3229
+tenparen 247D
+tenperiod 2491
+tenroman 2179
+tesh 02A7
+tet 05D8
+tetdagesh FB38
+tetdageshhebrew FB38
+tethebrew 05D8
+tetsecyrillic 04B5
+tevirhebrew 059B
+tevirlefthebrew 059B
+thabengali 09A5
+thadeva 0925
+thagujarati 0AA5
+thagurmukhi 0A25
+thalarabic 0630
+thalfinalarabic FEAC
+thanthakhatlowleftthai F898
+thanthakhatlowrightthai F897
+thanthakhatthai 0E4C
+thanthakhatupperleftthai F896
+theharabic 062B
+thehfinalarabic FE9A
+thehinitialarabic FE9B
+thehmedialarabic FE9C
+thereexists 2203
+therefore 2234
+theta 03B8
+theta1 03D1
+thetasymbolgreek 03D1
+thieuthacirclekorean 3279
+thieuthaparenkorean 3219
+thieuthcirclekorean 326B
+thieuthkorean 314C
+thieuthparenkorean 320B
+thirteencircle 246C
+thirteenparen 2480
+thirteenperiod 2494
+thonangmonthothai 0E11
+thook 01AD
+thophuthaothai 0E12
+thorn 00FE
+thothahanthai 0E17
+thothanthai 0E10
+thothongthai 0E18
+thothungthai 0E16
+thousandcyrillic 0482
+thousandsseparatorarabic 066C
+thousandsseparatorpersian 066C
+three 0033
+threearabic 0663
+threebengali 09E9
+threecircle 2462
+threecircleinversesansserif 278C
+threedeva 0969
+threeeighths 215C
+threegujarati 0AE9
+threegurmukhi 0A69
+threehackarabic 0663
+threehangzhou 3023
+threeideographicparen 3222
+threeinferior 2083
+threemonospace FF13
+threenumeratorbengali 09F6
+threeoldstyle F733
+threeparen 2476
+threeperiod 248A
+threepersian 06F3
+threequarters 00BE
+threequartersemdash F6DE
+threeroman 2172
+threesuperior 00B3
+threethai 0E53
+thzsquare 3394
+tihiragana 3061
+tikatakana 30C1
+tikatakanahalfwidth FF81
+tikeutacirclekorean 3270
+tikeutaparenkorean 3210
+tikeutcirclekorean 3262
+tikeutkorean 3137
+tikeutparenkorean 3202
+tilde 02DC
+tildebelowcmb 0330
+tildecmb 0303
+tildecomb 0303
+tildedoublecmb 0360
+tildeoperator 223C
+tildeoverlaycmb 0334
+tildeverticalcmb 033E
+timescircle 2297
+tipehahebrew 0596
+tipehalefthebrew 0596
+tippigurmukhi 0A70
+titlocyrilliccmb 0483
+tiwnarmenian 057F
+tlinebelow 1E6F
+tmonospace FF54
+toarmenian 0569
+tohiragana 3068
+tokatakana 30C8
+tokatakanahalfwidth FF84
+tonebarextrahighmod 02E5
+tonebarextralowmod 02E9
+tonebarhighmod 02E6
+tonebarlowmod 02E8
+tonebarmidmod 02E7
+tonefive 01BD
+tonesix 0185
+tonetwo 01A8
+tonos 0384
+tonsquare 3327
+topatakthai 0E0F
+tortoiseshellbracketleft 3014
+tortoiseshellbracketleftsmall FE5D
+tortoiseshellbracketleftvertical FE39
+tortoiseshellbracketright 3015
+tortoiseshellbracketrightsmall FE5E
+tortoiseshellbracketrightvertical FE3A
+totaothai 0E15
+tpalatalhook 01AB
+tparen 24AF
+trademark 2122
+trademarksans F8EA
+trademarkserif F6DB
+tretroflexhook 0288
+triagdn 25BC
+triaglf 25C4
+triagrt 25BA
+triagup 25B2
+ts 02A6
+tsadi 05E6
+tsadidagesh FB46
+tsadidageshhebrew FB46
+tsadihebrew 05E6
+tsecyrillic 0446
+tsere 05B5
+tsere12 05B5
+tsere1e 05B5
+tsere2b 05B5
+tserehebrew 05B5
+tserenarrowhebrew 05B5
+tserequarterhebrew 05B5
+tserewidehebrew 05B5
+tshecyrillic 045B
+tsuperior F6F3
+ttabengali 099F
+ttadeva 091F
+ttagujarati 0A9F
+ttagurmukhi 0A1F
+tteharabic 0679
+ttehfinalarabic FB67
+ttehinitialarabic FB68
+ttehmedialarabic FB69
+tthabengali 09A0
+tthadeva 0920
+tthagujarati 0AA0
+tthagurmukhi 0A20
+tturned 0287
+tuhiragana 3064
+tukatakana 30C4
+tukatakanahalfwidth FF82
+tusmallhiragana 3063
+tusmallkatakana 30C3
+tusmallkatakanahalfwidth FF6F
+twelvecircle 246B
+twelveparen 247F
+twelveperiod 2493
+twelveroman 217B
+twentycircle 2473
+twentyhangzhou 5344
+twentyparen 2487
+twentyperiod 249B
+two 0032
+twoarabic 0662
+twobengali 09E8
+twocircle 2461
+twocircleinversesansserif 278B
+twodeva 0968
+twodotenleader 2025
+twodotleader 2025
+twodotleadervertical FE30
+twogujarati 0AE8
+twogurmukhi 0A68
+twohackarabic 0662
+twohangzhou 3022
+twoideographicparen 3221
+twoinferior 2082
+twomonospace FF12
+twonumeratorbengali 09F5
+twooldstyle F732
+twoparen 2475
+twoperiod 2489
+twopersian 06F2
+tworoman 2171
+twostroke 01BB
+twosuperior 00B2
+twothai 0E52
+twothirds 2154
+u 0075
+uacute 00FA
+ubar 0289
+ubengali 0989
+ubopomofo 3128
+ubreve 016D
+ucaron 01D4
+ucircle 24E4
+ucircumflex 00FB
+ucircumflexbelow 1E77
+ucyrillic 0443
+udattadeva 0951
+udblacute 0171
+udblgrave 0215
+udeva 0909
+udieresis 00FC
+udieresisacute 01D8
+udieresisbelow 1E73
+udieresiscaron 01DA
+udieresiscyrillic 04F1
+udieresisgrave 01DC
+udieresismacron 01D6
+udotbelow 1EE5
+ugrave 00F9
+ugujarati 0A89
+ugurmukhi 0A09
+uhiragana 3046
+uhookabove 1EE7
+uhorn 01B0
+uhornacute 1EE9
+uhorndotbelow 1EF1
+uhorngrave 1EEB
+uhornhookabove 1EED
+uhorntilde 1EEF
+uhungarumlaut 0171
+uhungarumlautcyrillic 04F3
+uinvertedbreve 0217
+ukatakana 30A6
+ukatakanahalfwidth FF73
+ukcyrillic 0479
+ukorean 315C
+umacron 016B
+umacroncyrillic 04EF
+umacrondieresis 1E7B
+umatragurmukhi 0A41
+umonospace FF55
+underscore 005F
+underscoredbl 2017
+underscoremonospace FF3F
+underscorevertical FE33
+underscorewavy FE4F
+union 222A
+universal 2200
+uogonek 0173
+uparen 24B0
+upblock 2580
+upperdothebrew 05C4
+upsilon 03C5
+upsilondieresis 03CB
+upsilondieresistonos 03B0
+upsilonlatin 028A
+upsilontonos 03CD
+uptackbelowcmb 031D
+uptackmod 02D4
+uragurmukhi 0A73
+uring 016F
+ushortcyrillic 045E
+usmallhiragana 3045
+usmallkatakana 30A5
+usmallkatakanahalfwidth FF69
+ustraightcyrillic 04AF
+ustraightstrokecyrillic 04B1
+utilde 0169
+utildeacute 1E79
+utildebelow 1E75
+uubengali 098A
+uudeva 090A
+uugujarati 0A8A
+uugurmukhi 0A0A
+uumatragurmukhi 0A42
+uuvowelsignbengali 09C2
+uuvowelsigndeva 0942
+uuvowelsigngujarati 0AC2
+uvowelsignbengali 09C1
+uvowelsigndeva 0941
+uvowelsigngujarati 0AC1
+v 0076
+vadeva 0935
+vagujarati 0AB5
+vagurmukhi 0A35
+vakatakana 30F7
+vav 05D5
+vavdagesh FB35
+vavdagesh65 FB35
+vavdageshhebrew FB35
+vavhebrew 05D5
+vavholam FB4B
+vavholamhebrew FB4B
+vavvavhebrew 05F0
+vavyodhebrew 05F1
+vcircle 24E5
+vdotbelow 1E7F
+vecyrillic 0432
+veharabic 06A4
+vehfinalarabic FB6B
+vehinitialarabic FB6C
+vehmedialarabic FB6D
+vekatakana 30F9
+venus 2640
+verticalbar 007C
+verticallineabovecmb 030D
+verticallinebelowcmb 0329
+verticallinelowmod 02CC
+verticallinemod 02C8
+vewarmenian 057E
+vhook 028B
+vikatakana 30F8
+viramabengali 09CD
+viramadeva 094D
+viramagujarati 0ACD
+visargabengali 0983
+visargadeva 0903
+visargagujarati 0A83
+vmonospace FF56
+voarmenian 0578
+voicediterationhiragana 309E
+voicediterationkatakana 30FE
+voicedmarkkana 309B
+voicedmarkkanahalfwidth FF9E
+vokatakana 30FA
+vparen 24B1
+vtilde 1E7D
+vturned 028C
+vuhiragana 3094
+vukatakana 30F4
+w 0077
+wacute 1E83
+waekorean 3159
+wahiragana 308F
+wakatakana 30EF
+wakatakanahalfwidth FF9C
+wakorean 3158
+wasmallhiragana 308E
+wasmallkatakana 30EE
+wattosquare 3357
+wavedash 301C
+wavyunderscorevertical FE34
+wawarabic 0648
+wawfinalarabic FEEE
+wawhamzaabovearabic 0624
+wawhamzaabovefinalarabic FE86
+wbsquare 33DD
+wcircle 24E6
+wcircumflex 0175
+wdieresis 1E85
+wdotaccent 1E87
+wdotbelow 1E89
+wehiragana 3091
+weierstrass 2118
+wekatakana 30F1
+wekorean 315E
+weokorean 315D
+wgrave 1E81
+whitebullet 25E6
+whitecircle 25CB
+whitecircleinverse 25D9
+whitecornerbracketleft 300E
+whitecornerbracketleftvertical FE43
+whitecornerbracketright 300F
+whitecornerbracketrightvertical FE44
+whitediamond 25C7
+whitediamondcontainingblacksmalldiamond 25C8
+whitedownpointingsmalltriangle 25BF
+whitedownpointingtriangle 25BD
+whiteleftpointingsmalltriangle 25C3
+whiteleftpointingtriangle 25C1
+whitelenticularbracketleft 3016
+whitelenticularbracketright 3017
+whiterightpointingsmalltriangle 25B9
+whiterightpointingtriangle 25B7
+whitesmallsquare 25AB
+whitesmilingface 263A
+whitesquare 25A1
+whitestar 2606
+whitetelephone 260F
+whitetortoiseshellbracketleft 3018
+whitetortoiseshellbracketright 3019
+whiteuppointingsmalltriangle 25B5
+whiteuppointingtriangle 25B3
+wihiragana 3090
+wikatakana 30F0
+wikorean 315F
+wmonospace FF57
+wohiragana 3092
+wokatakana 30F2
+wokatakanahalfwidth FF66
+won 20A9
+wonmonospace FFE6
+wowaenthai 0E27
+wparen 24B2
+wring 1E98
+wsuperior 02B7
+wturned 028D
+wynn 01BF
+x 0078
+xabovecmb 033D
+xbopomofo 3112
+xcircle 24E7
+xdieresis 1E8D
+xdotaccent 1E8B
+xeharmenian 056D
+xi 03BE
+xmonospace FF58
+xparen 24B3
+xsuperior 02E3
+y 0079
+yaadosquare 334E
+yabengali 09AF
+yacute 00FD
+yadeva 092F
+yaekorean 3152
+yagujarati 0AAF
+yagurmukhi 0A2F
+yahiragana 3084
+yakatakana 30E4
+yakatakanahalfwidth FF94
+yakorean 3151
+yamakkanthai 0E4E
+yasmallhiragana 3083
+yasmallkatakana 30E3
+yasmallkatakanahalfwidth FF6C
+yatcyrillic 0463
+ycircle 24E8
+ycircumflex 0177
+ydieresis 00FF
+ydotaccent 1E8F
+ydotbelow 1EF5
+yeharabic 064A
+yehbarreearabic 06D2
+yehbarreefinalarabic FBAF
+yehfinalarabic FEF2
+yehhamzaabovearabic 0626
+yehhamzaabovefinalarabic FE8A
+yehhamzaaboveinitialarabic FE8B
+yehhamzaabovemedialarabic FE8C
+yehinitialarabic FEF3
+yehmedialarabic FEF4
+yehmeeminitialarabic FCDD
+yehmeemisolatedarabic FC58
+yehnoonfinalarabic FC94
+yehthreedotsbelowarabic 06D1
+yekorean 3156
+yen 00A5
+yenmonospace FFE5
+yeokorean 3155
+yeorinhieuhkorean 3186
+yerahbenyomohebrew 05AA
+yerahbenyomolefthebrew 05AA
+yericyrillic 044B
+yerudieresiscyrillic 04F9
+yesieungkorean 3181
+yesieungpansioskorean 3183
+yesieungsioskorean 3182
+yetivhebrew 059A
+ygrave 1EF3
+yhook 01B4
+yhookabove 1EF7
+yiarmenian 0575
+yicyrillic 0457
+yikorean 3162
+yinyang 262F
+yiwnarmenian 0582
+ymonospace FF59
+yod 05D9
+yoddagesh FB39
+yoddageshhebrew FB39
+yodhebrew 05D9
+yodyodhebrew 05F2
+yodyodpatahhebrew FB1F
+yohiragana 3088
+yoikorean 3189
+yokatakana 30E8
+yokatakanahalfwidth FF96
+yokorean 315B
+yosmallhiragana 3087
+yosmallkatakana 30E7
+yosmallkatakanahalfwidth FF6E
+yotgreek 03F3
+yoyaekorean 3188
+yoyakorean 3187
+yoyakthai 0E22
+yoyingthai 0E0D
+yparen 24B4
+ypogegrammeni 037A
+ypogegrammenigreekcmb 0345
+yr 01A6
+yring 1E99
+ysuperior 02B8
+ytilde 1EF9
+yturned 028E
+yuhiragana 3086
+yuikorean 318C
+yukatakana 30E6
+yukatakanahalfwidth FF95
+yukorean 3160
+yusbigcyrillic 046B
+yusbigiotifiedcyrillic 046D
+yuslittlecyrillic 0467
+yuslittleiotifiedcyrillic 0469
+yusmallhiragana 3085
+yusmallkatakana 30E5
+yusmallkatakanahalfwidth FF6D
+yuyekorean 318B
+yuyeokorean 318A
+yyabengali 09DF
+yyadeva 095F
+z 007A
+zaarmenian 0566
+zacute 017A
+zadeva 095B
+zagurmukhi 0A5B
+zaharabic 0638
+zahfinalarabic FEC6
+zahinitialarabic FEC7
+zahiragana 3056
+zahmedialarabic FEC8
+zainarabic 0632
+zainfinalarabic FEB0
+zakatakana 30B6
+zaqefgadolhebrew 0595
+zaqefqatanhebrew 0594
+zarqahebrew 0598
+zayin 05D6
+zayindagesh FB36
+zayindageshhebrew FB36
+zayinhebrew 05D6
+zbopomofo 3117
+zcaron 017E
+zcircle 24E9
+zcircumflex 1E91
+zcurl 0291
+zdot 017C
+zdotaccent 017C
+zdotbelow 1E93
+zecyrillic 0437
+zedescendercyrillic 0499
+zedieresiscyrillic 04DF
+zehiragana 305C
+zekatakana 30BC
+zero 0030
+zeroarabic 0660
+zerobengali 09E6
+zerodeva 0966
+zerogujarati 0AE6
+zerogurmukhi 0A66
+zerohackarabic 0660
+zeroinferior 2080
+zeromonospace FF10
+zerooldstyle F730
+zeropersian 06F0
+zerosuperior 2070
+zerothai 0E50
+zerowidthjoiner FEFF
+zerowidthnonjoiner 200C
+zerowidthspace 200B
+zeta 03B6
+zhbopomofo 3113
+zhearmenian 056A
+zhebrevecyrillic 04C2
+zhecyrillic 0436
+zhedescendercyrillic 0497
+zhedieresiscyrillic 04DD
+zihiragana 3058
+zikatakana 30B8
+zinorhebrew 05AE
+zlinebelow 1E95
+zmonospace FF5A
+zohiragana 305E
+zokatakana 30BE
+zparen 24B5
+zretroflexhook 0290
+zstroke 01B6
+zuhiragana 305A
+zukatakana 30BA
diff --git a/src/VellumPdf.Reader/Resources/ZapfDingbatsGlyphList.txt b/src/VellumPdf.Reader/Resources/ZapfDingbatsGlyphList.txt
new file mode 100644
index 00000000..ab52700e
--- /dev/null
+++ b/src/VellumPdf.Reader/Resources/ZapfDingbatsGlyphList.txt
@@ -0,0 +1,245 @@
+# -----------------------------------------------------------
+# Copyright 2002-2019 Adobe (http://www.adobe.com/).
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the
+# following conditions are met:
+#
+# Redistributions of source code must retain the above
+# copyright notice, this list of conditions and the following
+# disclaimer.
+#
+# Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials
+# provided with the distribution.
+#
+# Neither the name of Adobe nor the names of its contributors
+# may be used to endorse or promote products derived from this
+# software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------
+# Name: ITC Zapf Dingbats Glyph List
+# Table version: 2.0
+# Date: September 20, 2002
+# URL: https://github.com/adobe-type-tools/agl-aglfn
+#
+# Format: two semicolon-delimited fields:
+# (1) glyph name--upper/lowercase letters and digits
+# (2) Unicode scalar value--four uppercase hexadecimal digits
+#
+a100;275E
+a101;2761
+a102;2762
+a103;2763
+a104;2764
+a105;2710
+a106;2765
+a107;2766
+a108;2767
+a109;2660
+a10;2721
+a110;2665
+a111;2666
+a112;2663
+a117;2709
+a118;2708
+a119;2707
+a11;261B
+a120;2460
+a121;2461
+a122;2462
+a123;2463
+a124;2464
+a125;2465
+a126;2466
+a127;2467
+a128;2468
+a129;2469
+a12;261E
+a130;2776
+a131;2777
+a132;2778
+a133;2779
+a134;277A
+a135;277B
+a136;277C
+a137;277D
+a138;277E
+a139;277F
+a13;270C
+a140;2780
+a141;2781
+a142;2782
+a143;2783
+a144;2784
+a145;2785
+a146;2786
+a147;2787
+a148;2788
+a149;2789
+a14;270D
+a150;278A
+a151;278B
+a152;278C
+a153;278D
+a154;278E
+a155;278F
+a156;2790
+a157;2791
+a158;2792
+a159;2793
+a15;270E
+a160;2794
+a161;2192
+a162;27A3
+a163;2194
+a164;2195
+a165;2799
+a166;279B
+a167;279C
+a168;279D
+a169;279E
+a16;270F
+a170;279F
+a171;27A0
+a172;27A1
+a173;27A2
+a174;27A4
+a175;27A5
+a176;27A6
+a177;27A7
+a178;27A8
+a179;27A9
+a17;2711
+a180;27AB
+a181;27AD
+a182;27AF
+a183;27B2
+a184;27B3
+a185;27B5
+a186;27B8
+a187;27BA
+a188;27BB
+a189;27BC
+a18;2712
+a190;27BD
+a191;27BE
+a192;279A
+a193;27AA
+a194;27B6
+a195;27B9
+a196;2798
+a197;27B4
+a198;27B7
+a199;27AC
+a19;2713
+a1;2701
+a200;27AE
+a201;27B1
+a202;2703
+a203;2750
+a204;2752
+a205;276E
+a206;2770
+a20;2714
+a21;2715
+a22;2716
+a23;2717
+a24;2718
+a25;2719
+a26;271A
+a27;271B
+a28;271C
+a29;2722
+a2;2702
+a30;2723
+a31;2724
+a32;2725
+a33;2726
+a34;2727
+a35;2605
+a36;2729
+a37;272A
+a38;272B
+a39;272C
+a3;2704
+a40;272D
+a41;272E
+a42;272F
+a43;2730
+a44;2731
+a45;2732
+a46;2733
+a47;2734
+a48;2735
+a49;2736
+a4;260E
+a50;2737
+a51;2738
+a52;2739
+a53;273A
+a54;273B
+a55;273C
+a56;273D
+a57;273E
+a58;273F
+a59;2740
+a5;2706
+a60;2741
+a61;2742
+a62;2743
+a63;2744
+a64;2745
+a65;2746
+a66;2747
+a67;2748
+a68;2749
+a69;274A
+a6;271D
+a70;274B
+a71;25CF
+a72;274D
+a73;25A0
+a74;274F
+a75;2751
+a76;25B2
+a77;25BC
+a78;25C6
+a79;2756
+a7;271E
+a81;25D7
+a82;2758
+a83;2759
+a84;275A
+a85;276F
+a86;2771
+a87;2772
+a88;2773
+a89;2768
+a8;271F
+a90;2769
+a91;276C
+a92;276D
+a93;276A
+a94;276B
+a95;2774
+a96;2775
+a97;275B
+a98;275C
+a99;275D
+a9;2720
+# END
diff --git a/src/VellumPdf.Reader/VellumPdf.Reader.csproj b/src/VellumPdf.Reader/VellumPdf.Reader.csproj
index 1bd26a9d..2cd343b0 100644
--- a/src/VellumPdf.Reader/VellumPdf.Reader.csproj
+++ b/src/VellumPdf.Reader/VellumPdf.Reader.csproj
@@ -4,6 +4,19 @@
+
+
+
+ AdobeGlyphList.txt
+
+
+
+ ZapfDingbatsGlyphList.txt
+
+
+
VellumPdf.Reader
VellumPdf.Reader
diff --git a/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs
new file mode 100644
index 00000000..a1796c21
--- /dev/null
+++ b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs
@@ -0,0 +1,73 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using ConformanceEncoding = VellumPdf.Conformance.Rules.Fonts.SimpleFontEncoding;
+using ReaderEncodings = VellumPdf.Reader.Fonts.SimpleFontEncodings;
+
+namespace VellumPdf.Conformance.Tests.Fonts;
+
+///
+/// Proves the one claim SimpleFontEncodings' own class doc makes about
+/// src/VellumPdf.Conformance/Rules/Fonts/SimpleFontEncoding.cs: that the two copies diverge
+/// at exactly eight WinAnsi codes and seventeen MacRoman codes, and nowhere else. This project sees
+/// both assemblies' internals (Reader's AssemblyInfo.cs grants
+/// VellumPdf.Conformance.Tests; Conformance's grants its own tests), so it is the one place
+/// that can compare them directly instead of trusting the class doc's own count.
+///
+public sealed class ReaderEncodingParityTests
+{
+ private static List DifferingCodes(string?[] conformance, System.ReadOnlySpan reader)
+ {
+ var codes = new List();
+ for (var code = 0; code < 256; code++)
+ {
+ if (conformance[code] != reader[code])
+ codes.Add(code);
+ }
+ return codes;
+ }
+
+ [Fact]
+ public void Standard_matchesExactly()
+ {
+ Assert.Empty(DifferingCodes(ConformanceEncoding.Standard, ReaderEncodings.Standard));
+ }
+
+ [Fact]
+ public void WinAnsi_differsAtExactlyTheEightFootnoteCodes()
+ {
+ int[] expected = [0x7F, 0x81, 0x8D, 0x8F, 0x90, 0x9D, 0xA0, 0xAD];
+ Assert.Equal(expected, DifferingCodes(ConformanceEncoding.WinAnsi, ReaderEncodings.WinAnsi).Order());
+ }
+
+ [Fact]
+ public void MacRoman_differsAtExactlyTheSeventeenCodes()
+ {
+ int[] expected =
+ [
+ 0xAD, 0xB0, 0xB2, 0xB3, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBD,
+ 0xC3, 0xC5, 0xC6, 0xD7, 0xF0, 0xCA, 0xDB,
+ ];
+ Assert.Equal(expected.Order(), DifferingCodes(ConformanceEncoding.MacRoman, ReaderEncodings.MacRoman).Order());
+ }
+
+ // The class doc of both AdobeGlyphList copies asserts the two embedded AdobeGlyphList.txt
+ // resources are byte-identical; this test compares the two files directly rather than
+ // trusting that claim.
+ [Fact]
+ public void AdobeGlyphListResource_isByteIdenticalAcrossReaderAndConformance()
+ {
+ using var readerStream = typeof(VellumPdf.Reader.Fonts.AdobeGlyphList).Assembly
+ .GetManifestResourceStream("AdobeGlyphList.txt")!;
+ using var conformanceStream = typeof(VellumPdf.Conformance.Rules.Fonts.AdobeGlyphList).Assembly
+ .GetManifestResourceStream("AdobeGlyphList.txt")!;
+
+ using var readerBytes = new MemoryStream();
+ using var conformanceBytes = new MemoryStream();
+ readerStream.CopyTo(readerBytes);
+ conformanceStream.CopyTo(conformanceBytes);
+
+ Assert.Equal(conformanceBytes.Length, readerBytes.Length);
+ Assert.Equal(conformanceBytes.ToArray(), readerBytes.ToArray());
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs
new file mode 100644
index 00000000..152462a2
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs
@@ -0,0 +1,203 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+/// Pins against the AGL Specification's own
+/// algorithm and this reader's stated departures from it.
+public sealed class AdobeGlyphListTests
+{
+ [Theory]
+ [InlineData("A", "A")]
+ [InlineData("ffi", "ffi")]
+ [InlineData("f.alt", "f")]
+ [InlineData("uni0041.sc", "A")]
+ [InlineData("Alpha", "Α")]
+ public void TryMapToUnicode_singleResult(string name, string expected)
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode(name, out var unicode));
+ Assert.Equal(expected, unicode);
+ }
+
+ [Fact]
+ public void TryMapToUnicode_f_f_i_composesThreeChars()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode("f_f_i", out var unicode));
+ Assert.Equal("ffi", unicode);
+ }
+
+ [Fact]
+ public void TryMapToUnicode_uni00660066_composesFf()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode("uni00660066", out var unicode));
+ Assert.Equal("ff", unicode);
+ }
+
+ [Fact]
+ public void TryMapToUnicode_u1F600_givesTheSurrogatePair()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode("u1F600", out var unicode));
+ Assert.Equal(char.ConvertFromUtf32(0x1F600), unicode);
+ Assert.Equal(2, unicode.Length);
+ }
+
+ [Theory]
+ [InlineData("g12")]
+ [InlineData("cid5")]
+ [InlineData("uni004")] // short group
+ [InlineData("uni00410")] // 5 digits
+ [InlineData("uni0041x")]
+ [InlineData("uniD800")] // surrogate, via the uniXXXX route
+ [InlineData("uD800")] // surrogate, via the uXXXX..uXXXXXX route (TryUName's own guard)
+ [InlineData("u110000")] // past U+10FFFF
+ [InlineData("uni00e9")] // lowercase hex
+ [InlineData("a__b")]
+ [InlineData("_a")]
+ [InlineData("a_")]
+ [InlineData(".notdef")]
+ [InlineData("uni0000")]
+ [InlineData("f_g_nonexistent")]
+ [InlineData("uni")] // the "uni" prefix alone, no hex digits: neither route accepts it
+ [InlineData(".")] // truncates at the dot to an empty name
+ [InlineData("")]
+ public void TryMapToUnicode_rejectsTheseNames(string name)
+ {
+ Assert.False(AdobeGlyphList.TryMapToUnicode(name, out _));
+ }
+
+ [Fact]
+ public void TryMapToUnicode_u10FFFF_givesTheMaximumCodePoint()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode("u10FFFF", out var unicode));
+ Assert.Equal(char.ConvertFromUtf32(0x10FFFF), unicode);
+ }
+
+ [Fact]
+ public void TryMapToUnicode_uni00E9_true_lowercaseHexFalse()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode("uni00E9", out var unicode));
+ Assert.Equal("é", unicode);
+ Assert.False(AdobeGlyphList.TryMapToUnicode("uni00e9", out _));
+ }
+
+ [Fact]
+ public void TryMapToUnicode_lengthBoundary_uniGroups()
+ {
+ // 31 groups: 3 + 31*4 = 127 characters (accepted). 32 groups: 131 characters (rejected).
+ var accepted = "uni" + string.Concat(Enumerable.Repeat("0041", 31));
+ var rejected = "uni" + string.Concat(Enumerable.Repeat("0041", 32));
+ Assert.Equal(127, accepted.Length);
+ Assert.Equal(131, rejected.Length);
+ Assert.True(AdobeGlyphList.TryMapToUnicode(accepted, out _));
+ Assert.False(AdobeGlyphList.TryMapToUnicode(rejected, out _));
+ }
+
+ [Fact]
+ public void TryMapToUnicode_lengthBoundary_underscoreChain()
+ {
+ // 64 single-character components joined by 63 underscores: 64 + 63 = 127 characters.
+ // 65 components: 65 + 64 = 129 characters.
+ var accepted = string.Join('_', Enumerable.Repeat("a", 64));
+ var rejected = string.Join('_', Enumerable.Repeat("a", 65));
+ Assert.Equal(127, accepted.Length);
+ Assert.Equal(129, rejected.Length);
+ Assert.True(AdobeGlyphList.TryMapToUnicode(accepted, out var unicode));
+ Assert.Equal(64, unicode.Length);
+ Assert.False(AdobeGlyphList.TryMapToUnicode(rejected, out _));
+ }
+
+ [Fact]
+ public void TryMapToUnicode_underscoreAlone_false()
+ {
+ Assert.False(AdobeGlyphList.TryMapToUnicode("_", out _));
+ }
+
+ [Fact]
+ public void TryMapToUnicode_uni0041_B_composesAB()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode("uni0041_B", out var unicode));
+ Assert.Equal("AB", unicode);
+ }
+
+ [Fact]
+ public void TryMapToUnicode_lengthBoundary_exactly128Characters_accepted()
+ {
+ // 63 one-character components ("a") plus one two-character component ("AE"), joined by
+ // 63 underscores: 63 + 2 + 63 = 128, the exact upper bound. A ">=" off-by-one in the
+ // length gate would reject this, and only 127-character names would ever be exercised.
+ var components = Enumerable.Repeat("a", 63).Append("AE");
+ var name = string.Join('_', components);
+ Assert.Equal(128, name.Length);
+ Assert.True(AdobeGlyphList.TryMapToUnicode(name, out var unicode));
+ Assert.Equal(new string('a', 63) + "Æ", unicode);
+ }
+
+ [Fact]
+ public void TryMapToUnicode_uni0000_uni0000_composesTwoNulCharacters()
+ {
+ // The U+0000 rejection (this class's own remarks) fires only when the whole result is a
+ // single U+0000 character; two components that each individually resolve to U+0000
+ // concatenate to a two-character result, which that check does not catch.
+ Assert.True(AdobeGlyphList.TryMapToUnicode("uni0000_uni0000", out var unicode));
+ Assert.Equal("\0\0", unicode);
+ }
+
+ [Theory]
+ [InlineData("uniD7FF", true)]
+ [InlineData("uniD800", false)]
+ [InlineData("uniDFFF", false)]
+ [InlineData("uniE000", true)]
+ [InlineData("uniFFFF", true)]
+ public void TryMapToUnicode_uniSurrogateBoundary(string name, bool expected)
+ {
+ Assert.Equal(expected, AdobeGlyphList.TryMapToUnicode(name, out _));
+ }
+
+ [Fact]
+ public void TryMapToUnicode_singleComponentName_returnsTheSameInstanceEachCall()
+ {
+ // A single-component name returns the map's own string (or TryUniName/TryUName's own
+ // freshly built one) rather than a copy routed through a StringBuilder each call, so two
+ // calls with the same name share one instance; the multi-component path is the only one
+ // that composes.
+ Assert.True(AdobeGlyphList.TryMapToUnicode("ffi", out var first));
+ Assert.True(AdobeGlyphList.TryMapToUnicode("ffi", out var second));
+ Assert.Same(first, second);
+
+ Assert.True(AdobeGlyphList.TryMapToUnicode("A", out var firstA));
+ Assert.True(AdobeGlyphList.TryMapToUnicode("A", out var secondA));
+ Assert.Same(firstA, secondA);
+ }
+
+ [Fact]
+ public void ListSize_is4282()
+ {
+ Assert.Equal(4282, AdobeGlyphList.Count);
+ }
+
+ [Fact]
+ public void MultiCodePointEntryCount_is81()
+ {
+ // Read the embedded resource directly (the same file AdobeGlyphList.Count loads from) to
+ // count lines whose value carries more than one code point: a fact about the data file,
+ // not about the class's own lookup algorithm.
+ using var stream = typeof(AdobeGlyphList).Assembly.GetManifestResourceStream("AdobeGlyphList.txt")!;
+ using var reader = new StreamReader(stream);
+ var multiCodePoint = 0;
+ string? line;
+ while ((line = reader.ReadLine()) is not null)
+ {
+ if (line.Length == 0 || line[0] == '#')
+ continue;
+ var space = line.IndexOf(' ');
+ if (space <= 0)
+ continue;
+ var codes = line[(space + 1)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (codes.Length > 1)
+ multiCodePoint++;
+ }
+ Assert.Equal(81, multiCodePoint);
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs
new file mode 100644
index 00000000..afe6a7f1
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs
@@ -0,0 +1,275 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using CsCheck;
+using VellumPdf.Core;
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+///
+/// CsCheck property tests over mutated font dictionaries. The first drives
+/// directly: random /Subtypes, /Encoding
+/// shapes (including an indirect reference to an existing object and a two-hop chain neither
+/// this class nor follows), /Differences arrays mixing
+/// every element type, a non-array /Differences (an integer, a name, a dictionary, or an
+/// indirect reference), random /Widths lengths and element types including an indirect
+/// reference (to an existing object, a dangling one, or a null object) both as an element and as
+/// the whole /Widths value, random /Flags, random /ToUnicode shapes (direct
+/// and indirect), and random base font names including a 1 KiB one. The second drives
+/// itself, including its own indirect resolution of
+/// the font entry and its /Subtype. Both assert: no exception escapes, at most four
+/// distinct diagnostic codes are reported per font (400 to 402, plus one of 403/404), and
+/// over every byte value never throws.
+///
+public sealed class FontFuzzTests
+{
+ private static class FuzzBudget
+ {
+ private const long DefaultIterations = 3_000;
+
+ internal static long Iterations
+ {
+ get
+ {
+ var raw = Environment.GetEnvironmentVariable("VELLUMPDF_FUZZ_ITER");
+ return long.TryParse(raw, out var parsed) && parsed > 0 ? parsed : DefaultIterations;
+ }
+ }
+ }
+
+ // Object numbers pre-registered in the fixture document built by OpenFixture(), used by the
+ // indirect-shape generators below so a resolve inside SimpleFontReader.Create or
+ // GetFontReader hits an object present in the document rather than a dangling reference.
+ private const int EncodingDictObject = 50;
+ private const int EncodingChainHeadObject = 51;
+ private const int EncodingChainTargetObject = 52;
+ private const int ToUnicodeStreamObject = 60;
+ private const int NullObject = 61;
+ private const int WidthsNumberObject = 62;
+ private const int WidthsArrayObject = 63;
+ private const int FontDictObject = 100;
+ private const int NonDictionaryObject = 102;
+
+ private static PdfDocumentReader OpenFixture() => FontTestSupport.Open(
+ new FontTestSupport.Obj(EncodingDictObject, "<< /BaseEncoding /WinAnsiEncoding /Differences [65 /A] >>"),
+ new FontTestSupport.Obj(EncodingChainHeadObject, $"{EncodingChainTargetObject} 0 R"),
+ new FontTestSupport.Obj(EncodingChainTargetObject, "<< /BaseEncoding /MacRomanEncoding >>"),
+ new FontTestSupport.Obj(ToUnicodeStreamObject, "<< >>", "/CIDInit /ProcSet findresource begin\n"u8.ToArray()),
+ new FontTestSupport.Obj(NullObject, "null"),
+ new FontTestSupport.Obj(WidthsNumberObject, "123"),
+ new FontTestSupport.Obj(WidthsArrayObject, "[100 200 300]"),
+ new FontTestSupport.Obj(FontDictObject, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"),
+ new FontTestSupport.Obj(NonDictionaryObject, "42"));
+
+ // PdfName's own constructor rejects an empty string (ArgumentException); the case where a
+ // parsed PDF represents a bare "/" as a zero-length name never reaches PdfName's constructor
+ // through the parser either, so this generator does not attempt to build one.
+ private static readonly Gen NameGen = Gen.OneOf(
+ Gen.Const("A"), Gen.Const("space"), Gen.Const("g123"),
+ Gen.String[1, 12], Gen.String[120, 200]);
+
+ private static readonly Gen DifferencesElementGen = Gen.OneOf(
+ Gen.Int[-10, 300].Select(i => (PdfObject)new PdfInteger(i)),
+ NameGen.Select(n => (PdfObject)new PdfName(n)),
+ Gen.Int[0, 50].Select(i => (PdfObject)new PdfIndirectReference(i, 0)),
+ Gen.Double[-100, 100].Select(d => (PdfObject)new PdfReal(d)),
+ Gen.Const((PdfObject)new PdfDictionary()),
+ Gen.Const((PdfObject)new PdfArray()));
+
+ private static readonly Gen DifferencesGen =
+ DifferencesElementGen.Array[0, 12].Select(items => new PdfArray(items));
+
+ // /Differences present but not an array at all: an integer, a name, a dictionary, and an
+ // indirect reference that is still a reference after one hop (EncodingChainHeadObject's own
+ // content is a reference to EncodingChainTargetObject), the same shape
+ // Differences_selfReferentialChain_stillAReferenceAfterOneHop_reports401Once pins directly.
+ private static readonly Gen NonArrayDifferencesGen = Gen.OneOf(
+ Gen.Int[-10, 300].Select(i => (PdfObject)new PdfInteger(i)),
+ NameGen.Select(n => (PdfObject)new PdfName(n)),
+ Gen.Const((PdfObject)new PdfDictionary()),
+ Gen.Const((PdfObject)new PdfIndirectReference(EncodingChainHeadObject, 0)),
+ // Direct and one-hop null, which §7.3.7 makes equivalent to an absent /Differences: the
+ // encoding dictionary's other entries still apply and no 401 is reported.
+ Gen.Const((PdfObject)PdfNull.Instance),
+ Gen.Const((PdfObject)new PdfIndirectReference(NullObject, 0)));
+
+ private static readonly Gen DifferencesValueGen = Gen.OneOf(
+ DifferencesGen.Select(a => (PdfObject)a),
+ NonArrayDifferencesGen);
+
+ private static readonly Gen EncodingGen = Gen.OneOf(
+ Gen.Const((PdfObject?)null),
+ Gen.Const((PdfObject?)new PdfName("StandardEncoding")),
+ Gen.Const((PdfObject?)new PdfName("WinAnsiEncoding")),
+ Gen.Const((PdfObject?)new PdfName("MacRomanEncoding")),
+ Gen.Const((PdfObject?)new PdfName("Bogus")),
+ Gen.Const((PdfObject?)new PdfInteger(42)),
+ // Direct and one-hop null. §7.3.7 makes both the same as an absent /Encoding, so the font
+ // keeps its built-in or standard base encoding and reports nothing; the corpus reached
+ // that rule through /Widths only before these two arms.
+ Gen.Const((PdfObject?)PdfNull.Instance),
+ Gen.Const((PdfObject?)new PdfIndirectReference(NullObject, 0)),
+ // Resolves in one hop to the encoding dictionary at EncodingDictObject.
+ Gen.Const((PdfObject?)new PdfIndirectReference(EncodingDictObject, 0)),
+ // Resolves in one hop to ANOTHER reference (EncodingChainHeadObject's own content is
+ // "EncodingChainTargetObject 0 R"): the two-hop chain this reader does not follow.
+ Gen.Const((PdfObject?)new PdfIndirectReference(EncodingChainHeadObject, 0)),
+ DifferencesValueGen.Select(diffs =>
+ {
+ var dict = new PdfDictionary().Set(new PdfName("Differences"), diffs);
+ return (PdfObject?)dict;
+ }),
+ Gen.Select(Gen.OneOf(Gen.Const("WinAnsiEncoding"), Gen.Const("Bogus"), Gen.Const("MacRomanEncoding")),
+ DifferencesValueGen,
+ (baseName, diffs) => (PdfObject?)new PdfDictionary()
+ .Set(new PdfName("BaseEncoding"), new PdfName(baseName))
+ .Set(new PdfName("Differences"), diffs)));
+
+ // Includes a reference to an object the fixture defines (WidthsNumberObject, a bare integer),
+ // a dangling one, and one to a null object, so a /Widths element can resolve, dangle, or
+ // resolve to PdfNull, alongside the direct-value shapes.
+ private static readonly Gen WidthsElementGen = Gen.OneOf(
+ Gen.Int[-100, 2000].Select(i => (PdfObject)new PdfInteger(i)),
+ Gen.Double[-100, 2000].Select(d => (PdfObject)new PdfReal(d)),
+ NameGen.Select(n => (PdfObject)new PdfName(n)),
+ Gen.Const((PdfObject)new PdfIndirectReference(WidthsNumberObject, 0)),
+ Gen.Const((PdfObject)new PdfIndirectReference(999, 0)),
+ Gen.Const((PdfObject)new PdfIndirectReference(NullObject, 0)));
+
+ // /Widths itself as an indirect reference: to an array the fixture defines
+ // (WidthsArrayObject), a dangling one, and one to a null object.
+ private static readonly Gen WidthsGen = Gen.OneOf(
+ Gen.Const((PdfObject?)null),
+ WidthsElementGen.Array[0, 10].Select(items => (PdfObject?)new PdfArray(items)),
+ Gen.Const((PdfObject?)new PdfInteger(5)),
+ Gen.Const((PdfObject?)new PdfIndirectReference(WidthsArrayObject, 0)),
+ Gen.Const((PdfObject?)new PdfIndirectReference(999, 0)),
+ Gen.Const((PdfObject?)new PdfIndirectReference(NullObject, 0)));
+
+ private static readonly Gen BaseFontGen = Gen.OneOf(
+ Gen.Const("Helvetica"), Gen.Const("Symbol"), Gen.Const("ZapfDingbats"),
+ Gen.Const("Arial,Bold"), Gen.Const("Foo"), Gen.String[1, 20],
+ Gen.Const(new string('B', 1024)));
+
+ private static readonly Gen FlagsGen = Gen.OneOf(
+ Gen.Const(0), Gen.Const(4), Gen.Const(32), Gen.Const(36), Gen.Int[-1000, 1000]);
+
+ // Subtypes this reader's own Create() doesn't gate on (unlike GetFontReader, which decides
+ // whether to call Create at all): every value here still reaches Create, so this only varies
+ // whether the "trueType" branch of step 5 (the StandardEncoding fill) fires.
+ private static readonly Gen SubtypeGen = Gen.OneOf(
+ Gen.Const((PdfObject?)new PdfName("Type1")),
+ Gen.Const((PdfObject?)new PdfName("MMType1")),
+ Gen.Const((PdfObject?)new PdfName("TrueType")),
+ Gen.Const((PdfObject?)new PdfName("Type0")),
+ Gen.Const((PdfObject?)new PdfName("Type3")),
+ Gen.Const((PdfObject?)new PdfName("Bogus")),
+ Gen.Const((PdfObject?)new PdfInteger(7)),
+ Gen.Const((PdfObject?)null)); // omitted entirely
+
+ private static readonly Gen ToUnicodeGen = Gen.OneOf(
+ Gen.Const((PdfObject?)null),
+ Gen.Const((PdfObject?)new PdfStream("/CIDInit /ProcSet findresource begin\n"u8.ToArray())),
+ Gen.Const((PdfObject?)new PdfIndirectReference(ToUnicodeStreamObject, 0)));
+
+ private static readonly Gen FontDictGen = Gen.Select(
+ BaseFontGen, EncodingGen, WidthsGen, FlagsGen, SubtypeGen, ToUnicodeGen,
+ (baseFont, encoding, widths, flags, subtype, toUnicode) =>
+ {
+ var dict = new PdfDictionary();
+ if (subtype is not null)
+ dict.Set(PdfName.Subtype, subtype);
+ dict.Set(PdfName.BaseFont, baseFont);
+ if (encoding is not null)
+ dict.Set(PdfName.Encoding, encoding);
+ if (widths is not null)
+ {
+ dict.Set(new PdfName("FirstChar"), new PdfInteger(0));
+ dict.Set(new PdfName("LastChar"), new PdfInteger(widths is PdfArray a ? a.Count - 1 : -1));
+ dict.Set(new PdfName("Widths"), widths);
+ }
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(flags));
+ dict.Set(new PdfName("FontDescriptor"), descriptor);
+ if (toUnicode is not null)
+ dict.Set(new PdfName("ToUnicode"), toUnicode);
+ return dict;
+ });
+
+ private static void AssertOnlyDocumentedCodes(DiagnosticSink sink)
+ {
+ var distinctCodes = sink.Diagnostics.Select(d => d.Code).Distinct().ToList();
+ Assert.True(
+ distinctCodes.Count <= 4,
+ $"expected at most 4 distinct codes, got {distinctCodes.Count}: {string.Join(", ", distinctCodes)}");
+ foreach (var code in distinctCodes)
+ {
+ Assert.True(
+ code is PdfReaderDiagnosticCode.FontUnreadable
+ or PdfReaderDiagnosticCode.FontEncodingMalformed
+ or PdfReaderDiagnosticCode.FontWidthsMalformed
+ or PdfReaderDiagnosticCode.FontNoUnicodeRoute
+ or PdfReaderDiagnosticCode.UnmappedGlyphs,
+ $"unexpected code {code}");
+ }
+ }
+
+ private static void DecodeEveryByte(PdfFontReader reader)
+ {
+ for (var b = 0; b < 256; b++)
+ {
+ ReadOnlySpan bytes = [(byte)b];
+ var offset = 0;
+ reader.TryDecodeNext(bytes, ref offset, out _);
+ }
+ }
+
+ [Fact]
+ public void Create_neverThrows_reportsAtMostFourDistinctCodes_decodeNeverThrows()
+ {
+ using var doc = OpenFixture();
+ // threads: 1: every sample resolves against the one shared doc (the indirect /Encoding
+ // shapes each need an object already present in it to resolve against), and
+ // PdfDocumentReader's own object cache is a plain Dictionary, not built for concurrent
+ // access from multiple worker threads. Running serially keeps the test out of that
+ // cache's concurrency behaviour, which is not what this test is about.
+ FontDictGen.Sample(
+ fontDict =>
+ {
+ var sink = new DiagnosticSink(cap: 50);
+ var reader = SimpleFontReader.Create(doc, fontDict, null, null, sink, null);
+ AssertOnlyDocumentedCodes(sink);
+ DecodeEveryByte(reader);
+ },
+ iter: FuzzBudget.Iterations, threads: 1);
+ }
+
+ // Direct dictionaries drawn from FontDictGen exercise GetFontReader's own dispatch on
+ // /Subtype; the three indirect shapes exercise its own two ResolveValue calls (the font entry
+ // itself, then /Subtype) against an existing dictionary object, an existing non-dictionary
+ // object, and a dangling reference, none of which should ever escape as an exception.
+ private static readonly Gen FontEntryGen = Gen.OneOf(
+ FontDictGen.Select(d => (PdfObject)d),
+ Gen.Const((PdfObject)new PdfIndirectReference(FontDictObject, 0)),
+ Gen.Const((PdfObject)new PdfIndirectReference(NonDictionaryObject, 0)),
+ Gen.Const((PdfObject)new PdfIndirectReference(999, 0)),
+ Gen.Const((PdfObject)new PdfInteger(3)));
+
+ [Fact]
+ public void GetFontReader_neverThrows_reportsAtMostFourDistinctCodes_decodeNeverThrows()
+ {
+ using var doc = OpenFixture();
+ // threads: 1: see Create_neverThrows' own comment; FontCache adds its own plain
+ // Dictionary on top, written on every cache miss for the same shared doc.
+ FontEntryGen.Sample(
+ entry =>
+ {
+ var sink = new DiagnosticSink(cap: 50);
+ var reader = doc.GetFontReader(entry, sink, pageIndex: null);
+ AssertOnlyDocumentedCodes(sink);
+ if (reader is not null)
+ DecodeEveryByte(reader);
+ },
+ iter: FuzzBudget.Iterations, threads: 1);
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs
new file mode 100644
index 00000000..0d3b5233
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs
@@ -0,0 +1,122 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+///
+/// Shared fixture builders for the Fonts/ test classes: a minimal, hand-built PDF byte
+/// stream (the ContentInterpreterTests / PageTreeTests style: a raw text template
+/// per object, not VellumPdf.Document.PdfDocument) that gives
+/// a live
+/// to resolve indirect references through, with exact control over
+/// object shapes a document writer would never produce.
+///
+internal static class FontTestSupport
+{
+ internal readonly record struct Obj(int Num, string Dict, byte[]? Stream = null);
+
+ /// A one-page document with no fonts of its own; enough for tests that only need a
+ /// live to resolve direct (non-reference) objects
+ /// through.
+ internal static PdfDocumentReader OpenMinimal() => Open();
+
+ /// Opens a document built from , always including a minimal
+ /// catalog/page tree at objects 1 and 2 so startxref//Root resolve.
+ internal static PdfDocumentReader Open(params Obj[] objects)
+ {
+ var all = new List
+ {
+ new(1, "<< /Type /Catalog /Pages 2 0 R >>"),
+ new(2, "<< /Type /Pages /Kids [] /Count 0 >>"),
+ };
+ all.AddRange(objects);
+ return PdfReader.Open(BuildPdf(1, [.. all]));
+ }
+
+ ///
+ /// A one-page document whose object is a stream whose
+ /// /Length is an indirect reference to the next object, itself a stream with the same
+ /// shape, objects deep; the final object is a plain integer
+ /// terminus. Resolving re-enters
+ /// PdfDocumentReader.Resolve once per link (parsing a stream's own structure calls back
+ /// into resolution for its /Length), so a long enough chain throws
+ /// past MaxResolveDepth before the caller's own
+ /// dictionary-type check ever runs, the same shape XrefStreamTests uses to pin this
+ /// guard against a stack overflow.
+ ///
+ internal static byte[] BuildDeepIndirectLengthChain(int firstChainObject, int chainLen)
+ {
+ var ms = new MemoryStream();
+ void W(string s) => ms.Write(Encoding.ASCII.GetBytes(s));
+
+ W("%PDF-1.7\n");
+ var last = firstChainObject + chainLen;
+ var offsets = new int[last + 1];
+
+ offsets[1] = (int)ms.Position;
+ W("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
+ offsets[2] = (int)ms.Position;
+ W("2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
+
+ for (var k = firstChainObject; k < last; k++)
+ {
+ offsets[k] = (int)ms.Position;
+ W($"{k} 0 obj\n<< /Length {k + 1} 0 R >>\nstream\nx\nendstream\nendobj\n");
+ }
+ offsets[last] = (int)ms.Position;
+ W($"{last} 0 obj\n1\nendobj\n");
+
+ var xrefOffset = (int)ms.Position;
+ W($"xref\n0 {last + 1}\n");
+ W("0000000000 65535 f \n");
+ for (var k = 1; k <= last; k++)
+ W($"{offsets[k]:D10} 00000 n \n");
+ W($"trailer\n<< /Size {last + 1} /Root 1 0 R >>\n");
+ W($"startxref\n{xrefOffset}\n%%EOF\n");
+
+ return ms.ToArray();
+ }
+
+ private static byte[] BuildPdf(int rootObjectNumber, Obj[] objects)
+ {
+ var ms = new MemoryStream();
+ void W(string s) => ms.Write(Encoding.ASCII.GetBytes(s));
+
+ W("%PDF-1.7\n");
+
+ var maxNum = objects.Max(o => o.Num);
+ var offsets = new int?[maxNum + 1];
+ foreach (var obj in objects.OrderBy(o => o.Num))
+ {
+ offsets[obj.Num] = (int)ms.Position;
+ if (obj.Stream is null)
+ {
+ W($"{obj.Num} 0 obj\n{obj.Dict}\nendobj\n");
+ }
+ else
+ {
+ var trimmed = obj.Dict.TrimEnd();
+ var withLength = trimmed[..^2].TrimEnd() + $" /Length {obj.Stream.Length} >>";
+ W($"{obj.Num} 0 obj\n{withLength}\nstream\n");
+ ms.Write(obj.Stream);
+ W("\nendstream\nendobj\n");
+ }
+ }
+
+ var xrefOffset = (int)ms.Position;
+ W($"xref\n0 {maxNum + 1}\n");
+ W("0000000000 65535 f \n");
+ for (var i = 1; i <= maxNum; i++)
+ {
+ W(offsets[i] is { } offset
+ ? $"{offset:D10} 00000 n \n"
+ : "0000000000 65535 f \n");
+ }
+ W($"trailer\n<< /Size {maxNum + 1} /Root {rootObjectNumber} 0 R >>\n");
+ W($"startxref\n{xrefOffset}\n%%EOF\n");
+
+ return ms.ToArray();
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs
new file mode 100644
index 00000000..e811fe37
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs
@@ -0,0 +1,277 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+///
+/// Pins cell by cell. Every value here was read directly from a
+/// rendered image of ISO 32000-2:2020 Annex D.2 (not from this reader's own output, and not from
+/// src/VellumPdf.Conformance/Rules/Fonts/SimpleFontEncoding.cs).
+///
+public sealed class SimpleFontEncodingsTests
+{
+ [Fact]
+ public void WinAnsi_pinnedCells()
+ {
+ var t = SimpleFontEncodings.WinAnsi;
+ Assert.Equal("A", t[0x41]);
+ Assert.Equal("quotesingle", t[0x27]);
+ Assert.Equal("grave", t[0x60]);
+ // Footnote 3 (bullet fill): all unused codes above octal 40 map to bullet.
+ Assert.Equal("bullet", t[0x7F]);
+ Assert.Equal("Euro", t[0x80]);
+ Assert.Equal("bullet", t[0x81]);
+ Assert.Equal("bullet", t[0x8D]);
+ Assert.Equal("bullet", t[0x8F]);
+ Assert.Equal("bullet", t[0x90]);
+ Assert.Equal("bullet", t[0x95]); // the one code the footnote names specifically.
+ Assert.Equal("bullet", t[0x9D]);
+ // Footnotes 5 and 6.
+ Assert.Equal("space", t[0xA0]);
+ Assert.Equal("hyphen", t[0xAD]);
+ Assert.Equal("ydieresis", t[0xFF]);
+ }
+
+ [Fact]
+ public void WinAnsi_nonNullCount_is224_everyCodeFrom0x20To0xFF()
+ {
+ var t = SimpleFontEncodings.WinAnsi;
+ var nonNull = 0;
+ for (var i = 0x20; i <= 0xFF; i++)
+ {
+ Assert.NotNull(t[i]);
+ nonNull++;
+ }
+ Assert.Equal(224, nonNull);
+ for (var i = 0x00; i <= 0x1F; i++)
+ Assert.Null(t[i]);
+ }
+
+ [Fact]
+ public void Standard_pinnedCells()
+ {
+ var t = SimpleFontEncodings.Standard;
+ Assert.Equal("quoteright", t[0x27]); // the discriminating cell against WinAnsi's quotesingle.
+ Assert.Equal("quoteleft", t[0x60]);
+ Assert.Equal("fraction", t[0xA4]);
+ Assert.Equal("fi", t[0xAE]);
+ Assert.Equal("fl", t[0xAF]);
+ Assert.Equal("oe", t[0xFA]);
+ Assert.Null(t[0xFF]);
+ Assert.Null(t[0x7F]);
+ }
+
+ [Fact]
+ public void Standard_nonNullCount_is149()
+ {
+ var t = SimpleFontEncodings.Standard;
+ var nonNull = 0;
+ for (var i = 0; i < 256; i++)
+ if (t[i] is not null)
+ nonNull++;
+ Assert.Equal(149, nonNull);
+ }
+
+ [Fact]
+ public void MacRoman_pinnedCells()
+ {
+ var t = SimpleFontEncodings.MacRoman;
+ Assert.Equal("Adieresis", t[0x80]);
+ // Footnote 6's dual mapping: MacRoman 0312 (octal) also reads as space.
+ Assert.Equal("space", t[0xCA]);
+ // Footnote 1: Annex D.2 and its own text both read "currency" here, not the Euro sign
+ // Apple's own later Mac OS Roman revision substituted.
+ Assert.Equal("currency", t[0xDB]);
+ Assert.Equal("fi", t[0xDE]);
+ Assert.Equal("fl", t[0xDF]);
+ Assert.Equal("caron", t[0xFF]);
+ Assert.Null(t[0xF0]); // Annex D.2 lists no glyph at this code (Table 113's own "apple").
+ }
+
+ [Theory]
+ // The 15 Table 113 cells the Conformance copy folds into its own MacRoman table (ISO
+ // 32000-2's own (1, 0) cmap-fallback table, not part of Annex D.2's MacRomanEncoding), pinned
+ // as undefined here by re-rendering Annex D.2 pp. 854-858 and finding no row for the name at
+ // this code.
+ [InlineData(0xAD)] // notequal
+ [InlineData(0xB0)] // infinity
+ [InlineData(0xB2)] // lessequal
+ [InlineData(0xB3)] // greaterequal
+ [InlineData(0xB6)] // partialdiff
+ [InlineData(0xB7)] // summation
+ [InlineData(0xB8)] // product
+ [InlineData(0xB9)] // pi
+ [InlineData(0xBA)] // integral
+ [InlineData(0xBD)] // Omega
+ [InlineData(0xC3)] // radical
+ [InlineData(0xC5)] // approxequal
+ [InlineData(0xC6)] // Delta
+ [InlineData(0xD7)] // lozenge
+ [InlineData(0xF0)] // apple
+ public void MacRoman_table113CellsAreUndefined(int code)
+ {
+ Assert.Null(SimpleFontEncodings.MacRoman[code]);
+ }
+
+ [Fact]
+ public void MacRoman_nonNullCount_is208()
+ {
+ // This reader's own count of the rendered Annex D.2 table: 224 codes 0x20-0xFF, minus
+ // 0x7F (undefined in MacRoman, unlike WinAnsi), minus the 15 Table 113 cells above.
+ var t = SimpleFontEncodings.MacRoman;
+ var nonNull = 0;
+ for (var i = 0x20; i <= 0xFF; i++)
+ if (t[i] is not null)
+ nonNull++;
+ Assert.Equal(208, nonNull);
+ }
+
+ [Fact]
+ public void Symbol_pinnedCells()
+ {
+ var t = SymbolFontMetrics.SymbolEncoding;
+ Assert.Equal("space", t[0x20]);
+ Assert.Equal("universal", t[0x22]);
+ Assert.Equal("Alpha", t[0x41]);
+ Assert.Equal("alpha", t[0x61]);
+ Assert.Null(t[0x80]);
+ Assert.Null(t[0x8D]);
+ Assert.Null(t[0x8E]);
+ Assert.Equal("Upsilon1", t[0xA1]);
+ Assert.Equal("infinity", t[0xA5]);
+ Assert.Equal("gradient", t[0xD1]);
+ Assert.Equal("integral", t[0xF2]);
+ Assert.Equal("bracerightbt", t[0xFE]);
+ Assert.Null(t[0xFF]);
+ }
+
+ [Fact]
+ public void Symbol_nonNullCount_is189()
+ {
+ var t = SymbolFontMetrics.SymbolEncoding;
+ var nonNull = 0;
+ for (var i = 0; i < 256; i++)
+ if (t[i] is not null)
+ nonNull++;
+ Assert.Equal(189, nonNull);
+ }
+
+ [Fact]
+ public void ZapfDingbats_pinnedCells()
+ {
+ var t = SymbolFontMetrics.ZapfDingbatsEncoding;
+ Assert.Equal("space", t[0x20]);
+ Assert.Equal("a2", t[0x22]);
+ Assert.Equal("a10", t[0x41]);
+ Assert.Equal("a60", t[0x61]);
+ Assert.Equal("a89", t[0x80]); // AFM-only code, not in Annex D.6.
+ Assert.Equal("a96", t[0x8D]); // AFM-only code, not in Annex D.6.
+ Assert.Null(t[0x8E]);
+ Assert.Equal("a101", t[0xA1]);
+ Assert.Equal("a106", t[0xA5]);
+ Assert.Equal("a157", t[0xD1]);
+ Assert.Equal("a183", t[0xF2]);
+ Assert.Equal("a191", t[0xFE]);
+ Assert.Null(t[0xFF]);
+ }
+
+ [Fact]
+ public void ZapfDingbats_nonNullCount_is202()
+ {
+ var t = SymbolFontMetrics.ZapfDingbatsEncoding;
+ var nonNull = 0;
+ for (var i = 0; i < 256; i++)
+ if (t[i] is not null)
+ nonNull++;
+ Assert.Equal(202, nonNull);
+ }
+
+ [Theory]
+ [InlineData("StandardEncoding")]
+ [InlineData("WinAnsiEncoding")]
+ [InlineData("MacRomanEncoding")]
+ [InlineData("MacExpertEncoding")]
+ public void TryGetNamed_recognisesTheFourNames(string name)
+ {
+ Assert.True(SimpleFontEncodings.TryGetNamed(name, out _));
+ }
+
+ [Theory]
+ [InlineData("StandardEncodingX")]
+ [InlineData("")]
+ [InlineData("WinAnsi")]
+ public void TryGetNamed_rejectsAnythingElse(string name)
+ {
+ Assert.False(SimpleFontEncodings.TryGetNamed(name, out _));
+ }
+
+ [Fact]
+ public void MacExpert_isAllNull()
+ {
+ var t = SimpleFontEncodings.MacExpert;
+ for (var i = 0; i < 256; i++)
+ Assert.Null(t[i]);
+ }
+
+ [Fact]
+ public void WinAnsi_definesEveryCode_thatStandardDefines()
+ {
+ // So §9.6.5.4's StandardEncoding fill (SimpleFontReader) changes nothing over a WinAnsi
+ // base table; the twelve cells it does change are all MacRoman's (next test).
+ for (var code = 0; code < 256; code++)
+ {
+ if (SimpleFontEncodings.Standard[code] is not null)
+ Assert.NotNull(SimpleFontEncodings.WinAnsi[code]);
+ }
+ }
+
+ [Fact]
+ public void MacRoman_leavesExactlyTwelveStandardCodes_undefined()
+ {
+ // The cells §9.6.5.4's StandardEncoding fill adds over a /BaseEncoding /MacRomanEncoding
+ // table, read off Annex D.2: each is a StandardEncoding column entry whose MacRoman column
+ // is blank.
+ var expected = new Dictionary
+ {
+ [0xAD] = "guilsinglright",
+ [0xB2] = "dagger",
+ [0xB3] = "daggerdbl",
+ [0xB6] = "paragraph",
+ [0xB7] = "bullet",
+ [0xB8] = "quotesinglbase",
+ [0xB9] = "quotedblbase",
+ [0xBA] = "quotedblright",
+ [0xBD] = "perthousand",
+ [0xC3] = "circumflex",
+ [0xC5] = "macron",
+ [0xC6] = "breve",
+ };
+
+ var actual = new Dictionary();
+ for (var code = 0; code < 256; code++)
+ {
+ if (SimpleFontEncodings.Standard[code] is { } name && SimpleFontEncodings.MacRoman[code] is null)
+ actual[code] = name;
+ }
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void SharedStatics_areImmutable_acrossFonts()
+ {
+ // A per-font table is always a fresh copy (SimpleFontReader.ToArray()s the shared span
+ // before applying /Differences). Building the copy and mutating it must never affect the
+ // shared static a later font's own copy is built from.
+ var perFont = SimpleFontEncodings.WinAnsi.ToArray();
+ perFont[0x41] = "B";
+ Assert.Equal("B", perFont[0x41]);
+
+ Assert.Equal("A", SimpleFontEncodings.WinAnsi[0x41]);
+
+ var second = SimpleFontEncodings.WinAnsi.ToArray();
+ Assert.Equal("A", second[0x41]);
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs
new file mode 100644
index 00000000..73896efe
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs
@@ -0,0 +1,1192 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using VellumPdf.Core;
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+///
+/// Exercises against hand-built font dictionaries (the
+/// style), covering §9.6.5's encoding resolution,
+/// /Differences, /Widths, and the diagnostics each malformation reports.
+///
+public sealed class SimpleFontReaderTests
+{
+ private static DecodedGlyph Decode(PdfFontReader reader, byte code)
+ {
+ ReadOnlySpan bytes = [code];
+ var offset = 0;
+ Assert.True(reader.TryDecodeNext(bytes, ref offset, out var glyph));
+ Assert.Equal(1, offset);
+ return glyph;
+ }
+
+ private static SimpleFontReader Build(
+ PdfDocumentReader doc, PdfDictionary fontDict, DiagnosticSink sink,
+ int? objectNumber = null, int? generation = null, int? pageIndex = null) =>
+ SimpleFontReader.Create(doc, fontDict, objectNumber, generation, sink, pageIndex);
+
+ private static PdfDictionary Type1(string baseFont) =>
+ new PdfDictionary().Set(PdfName.Subtype, "Type1").Set(PdfName.BaseFont, baseFont);
+
+ // ── 1: no /Encoding, no /Widths ──────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Helvetica_noEncoding_noWidths_nonsymbolic()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var reader = Build(doc, Type1("Helvetica"), sink);
+
+ var a = Decode(reader, 0x41);
+ Assert.Equal("A", a.Unicode);
+ Assert.Equal(667, a.Width);
+
+ // The discriminating cell against WinAnsi's quotesingle: StandardEncoding's 0x27 is
+ // quoteright, U+2019, not the ASCII apostrophe.
+ var quote = Decode(reader, 0x27);
+ Assert.Equal("’", quote.Unicode);
+
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ // ── 2: /Encoding /WinAnsiEncoding ────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Helvetica_winAnsiEncoding()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, "WinAnsiEncoding");
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("'", Decode(reader, 0x27).Unicode);
+ Assert.Equal("€", Decode(reader, 0x80).Unicode);
+ Assert.Equal(" ", Decode(reader, 0xA0).Unicode);
+ Assert.Equal("-", Decode(reader, 0xAD).Unicode);
+ Assert.Equal("•", Decode(reader, 0x7F).Unicode);
+ Assert.Equal("•", Decode(reader, 0x81).Unicode);
+ }
+
+ // ── 3: /Encoding /MacRomanEncoding ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Helvetica_macRomanEncoding()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, "MacRomanEncoding");
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(" ", Decode(reader, 0xCA).Unicode);
+ Assert.Equal("¤", Decode(reader, 0xDB).Unicode); // currency
+ }
+
+ // ── 4: indirect /Encoding, /BaseEncoding + /Differences ─────────────────────────────────────
+
+ [Fact]
+ public void IndirectEncodingDictionary_baseEncodingAndDifferences()
+ {
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica "
+ + "/Encoding 7 0 R >>"),
+ new FontTestSupport.Obj(7, "<< /Type /Encoding /BaseEncoding /WinAnsiEncoding "
+ + "/Differences [65 /Bsmall 66 /C /D 200 /Euro] >>"));
+
+ var sink = new DiagnosticSink(50);
+ var fontDict = (PdfDictionary)doc.Resolve(5)!;
+ var reader = Build(doc, fontDict, sink, objectNumber: 5, generation: 0);
+
+ Assert.Equal("\uF762", Decode(reader, 0x41).Unicode); // Bsmall, AGL private-use mapping.
+ Assert.Equal("C", Decode(reader, 0x42).Unicode);
+ Assert.Equal("D", Decode(reader, 0x43).Unicode);
+ Assert.Equal("€", Decode(reader, 0xC8).Unicode); // Euro
+ Assert.Equal("D", Decode(reader, 0x44).Unicode); // unchanged WinAnsi "D", never touched.
+ }
+
+ // ── 5: /Differences malformations ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Differences_codeOutOfRange_reports401Once_tableUnchanged()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray().Add(new PdfInteger(300)).Add(new PdfName("A"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("A", Decode(reader, 0x41).Unicode); // StandardEncoding's own 0x41, untouched.
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontEncodingMalformed, d.Code);
+ }
+
+ [Fact]
+ public void Differences_overflowPast255_assignsUpTo255_reports401Once()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray()
+ .Add(new PdfInteger(250))
+ .Add(new PdfName("A")).Add(new PdfName("B")).Add(new PdfName("C"))
+ .Add(new PdfName("D")).Add(new PdfName("E")).Add(new PdfName("F"))
+ .Add(new PdfName("G"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("A", Decode(reader, 250).Unicode);
+ Assert.Equal("F", Decode(reader, 255).Unicode);
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontEncodingMalformed, d.Code);
+ }
+
+ [Fact]
+ public void Differences_unresolvedElementType_reports401WithIndirectReferenceMessage()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray()
+ .Add(new PdfInteger(65)).Add(new PdfIndirectReference(5, 0));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("A", Decode(reader, 0x41).Unicode); // kept its base StandardEncoding name.
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontEncodingMalformed, d.Code);
+ Assert.Equal(
+ "/Differences contains an indirect reference, which this reader does not follow "
+ + "inside /Differences.",
+ d.Message);
+ }
+
+ [Fact]
+ public void Differences_nameLongerThanBound_reports401Once_codeStaysUndefined()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var longName = new string('a', 129);
+ var differences = new PdfArray().Add(new PdfInteger(65)).Add(new PdfName(longName));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var reader = Build(doc, fontDict, sink);
+
+ // Decoding the now-unmapped 0x41 also trips 404 (Helvetica's other codes are mapped),
+ // which is a separate, legitimate condition; this checks for the 401 specifically.
+ Assert.Null(Decode(reader, 0x41).Unicode);
+ Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ }
+
+ [Theory]
+ [InlineData("dictionary")]
+ [InlineData("integer")]
+ public void Differences_presentButNotAnArray_reports401Once(string shape)
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ PdfObject differences = shape switch
+ {
+ "dictionary" => new PdfDictionary(),
+ "integer" => new PdfInteger(7),
+ _ => throw new ArgumentOutOfRangeException(nameof(shape)),
+ };
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Contains("not an array", d.Message);
+ }
+
+ [Fact]
+ public void Differences_selfReferentialChain_stillAReferenceAfterOneHop_reports401Once()
+ {
+ // Object 4's own content is "5 0 R": resolving /Differences (a reference to object 4)
+ // takes exactly one hop and returns that value unresolved, still a PdfIndirectReference,
+ // the same unresolved-second-hop shape /BaseEncoding can carry, exercised here for
+ // /Differences itself rather than being silently dropped as if absent.
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(4, "5 0 R"),
+ new FontTestSupport.Obj(5, "[1 2 3]"));
+ var sink = new DiagnosticSink(50);
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), new PdfIndirectReference(4, 0));
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Contains("not an array", d.Message);
+ }
+
+ [Fact]
+ public void Differences_badElement_stopsApplyingArray_laterNamesKeepBaseEncoding()
+ {
+ // /Differences [65 /A 9 0 R /zcaron /Zcaron]: object 9 does not exist, so the reference is
+ // reported and the array stops being applied there; /zcaron and /Zcaron must keep their
+ // StandardEncoding names rather than landing on B (0x42) and C (0x43), which is where
+ // they would land if this reader resumed after the bad element with the running code
+ // left unchanged.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray()
+ .Add(new PdfInteger(65)).Add(new PdfName("A"))
+ .Add(new PdfIndirectReference(9, 0))
+ .Add(new PdfName("zcaron")).Add(new PdfName("Zcaron"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("A", Decode(reader, 0x41).Unicode); // applied before the bad element.
+ Assert.Equal("B", Decode(reader, 0x42).Unicode); // StandardEncoding, not overwritten.
+ Assert.Equal("C", Decode(reader, 0x43).Unicode); // StandardEncoding, not overwritten.
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Contains("does not follow inside /Differences", d.Message);
+ }
+
+ [Fact]
+ public void Differences_realElement_reports401WithExactMessage()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray().Add(new PdfReal(65.0)).Add(new PdfName("A"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Equal(
+ "/Differences contains an element that is neither an integer nor a name "
+ + "(the real number 65).",
+ d.Message);
+ }
+
+ // A nested array and a null are both types this reader defines and handles elsewhere, so the
+ // catch-all wording ("a type this reader does not recognise") would be false for either.
+ [Fact]
+ public void Differences_nestedArrayElement_reports401WithExactMessage()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray()
+ .Add(new PdfInteger(65))
+ .Add(new PdfArray().Add(new PdfName("A")))
+ .Add(new PdfName("B"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Equal(
+ "/Differences contains an element that is neither an integer nor a name "
+ + "(a nested array).",
+ d.Message);
+ }
+
+ // §9.6.5.1 makes each code "the first index in a sequence of character codes to be changed",
+ // so an array opening with a name names no starting code. The reader starts it at 0 rather
+ // than discarding the array, and says nothing.
+ [Fact]
+ public void Differences_arrayOpeningWithAName_startsAtCodeZero_withNoDiagnostic()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray().Add(new PdfName("A")).Add(new PdfName("B"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var font = Build(doc, fontDict, sink);
+
+ Assert.Empty(sink.Diagnostics);
+ Assert.Equal("A", Decode(font, 0x00).Unicode);
+ Assert.Equal("B", Decode(font, 0x01).Unicode);
+ }
+
+ [Fact]
+ public void Differences_nullElement_reports401WithExactMessage()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray()
+ .Add(new PdfInteger(65))
+ .Add(PdfNull.Instance)
+ .Add(new PdfName("B"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Equal(
+ "/Differences contains an element that is neither an integer nor a name (a null).",
+ d.Message);
+ }
+
+ [Fact]
+ public void Differences_indirectReferenceElement_reports401WithExactMessage()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray().Add(new PdfIndirectReference(1, 0)).Add(new PdfName("A"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Equal(
+ "/Differences contains an indirect reference, which this reader does not follow "
+ + "inside /Differences.",
+ d.Message);
+ }
+
+ // ── 6: /Encoding shapes ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Encoding_unknownName_reports401_usesStandardEncoding()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, "Bogus");
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("’", Decode(reader, 0x27).Unicode); // StandardEncoding's quoteright.
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontEncodingMalformed, d.Code);
+ }
+
+ [Fact]
+ public void Encoding_integer_reports401()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, new PdfInteger(42));
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontEncodingMalformed, d.Code);
+ }
+
+ [Fact]
+ public void Encoding_standardEncodingName_acceptedSilently()
+ {
+ // Table D.1's own note ("PDF processors shall not have a predefined encoding named
+ // StandardEncoding") is about built-in encodings, not about what a font's /Encoding may
+ // name; accepting it here is a deliberate leniency (see the class doc).
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, "StandardEncoding");
+ Build(doc, fontDict, sink);
+
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ [Fact]
+ public void Encoding_chainedBaseEncodingReference_reports401WithReferenceMessage()
+ {
+ // 4 0 R -> 5 0 R -> /WinAnsiEncoding: resolving /BaseEncoding (a reference to object 4)
+ // takes one hop and returns object 4's own content, itself the reference "5 0 R", never
+ // following on to the name at object 5. The message must say so rather than "names an
+ // encoding this reader does not know", which is true of a bad name, not an unresolved
+ // reference.
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(4, "5 0 R"),
+ new FontTestSupport.Obj(5, "/WinAnsiEncoding"));
+ var sink = new DiagnosticSink(50);
+ var encoding = new PdfDictionary().Set(new PdfName("BaseEncoding"), new PdfIndirectReference(4, 0));
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ Assert.Contains("indirect reference this reader does not follow past one hop", d.Message);
+ }
+
+ // ── 7: symbolic flag ─────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void SymbolicFlagSet_noEncoding_notEmbedded_allCellsNull_reports403()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(4));
+ var fontDict = Type1("Foo").Set(new PdfName("FontDescriptor"), descriptor);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0x41).Unicode);
+ // "Foo" is also not a standard 14 font, so FontWidthsMalformed fires alongside the 403
+ // this test is pinning; both are legitimate for this font, so this checks for the 403
+ // specifically rather than asserting it is the only diagnostic.
+ Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontNoUnicodeRoute);
+ }
+
+ [Fact]
+ public void FlagsNonsymbolicOnly_usesStandardEncoding()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(32));
+ var fontDict = Type1("Foo").Set(new PdfName("FontDescriptor"), descriptor);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("’", Decode(reader, 0x27).Unicode); // StandardEncoding's quoteright.
+ }
+
+ [Fact]
+ public void FlagsBothSymbolicAndNonsymbolic_symbolicWins()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(36));
+ var fontDict = Type1("Foo").Set(new PdfName("FontDescriptor"), descriptor);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0x41).Unicode); // all-null table, symbolic with no encoding.
+ }
+
+ // ── 8: embedded TrueType, no /Encoding ───────────────────────────────────────────────────────
+
+ [Fact]
+ public void EmbeddedNonsymbolicTrueType_noEncoding_usesStandardEncoding()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary()
+ .Set(new PdfName("Flags"), new PdfInteger(32))
+ .Set(new PdfName("FontFile2"), new PdfStream([1, 2, 3]));
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), descriptor);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("’", Decode(reader, 0x27).Unicode); // the stated deviation.
+ }
+
+ // Discriminating cell for §9.6.5.4's closing rule: StandardEncoding's 0xB2 is dagger, a cell
+ // Annex D.2's MacRoman column leaves blank (MacRomanEncoding puts dagger at 0xA0 instead).
+ private static PdfDictionary MacRomanBaseDictionary() =>
+ new PdfDictionary().Set(new PdfName("BaseEncoding"), "MacRomanEncoding");
+
+ private static PdfDictionary NonsymbolicDescriptor() =>
+ new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(32));
+
+ [Fact]
+ public void NonsymbolicTrueType_dictionaryWithMacRomanBase_fillsUndefinedCellsFromStandard()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), NonsymbolicDescriptor())
+ .Set(PdfName.Encoding, MacRomanBaseDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("†", Decode(reader, 0xB2).Unicode); // filled from StandardEncoding.
+ Assert.Equal("†", Decode(reader, 0xA0).Unicode); // MacRoman's own dagger, untouched.
+ Assert.Equal("'", Decode(reader, 0x27).Unicode); // MacRoman's quotesingle wins over Standard's quoteright.
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ }
+
+ [Fact]
+ public void NonsymbolicTrueType_namedMacRomanEncoding_isNotFilledFromStandard()
+ {
+ // The fill belongs to §9.6.5.4's dictionary bullet only; a name /Encoding takes Annex D.2's
+ // MacRoman column as it stands.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), NonsymbolicDescriptor())
+ .Set(PdfName.Encoding, "MacRomanEncoding");
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0xB2).Unicode);
+ Assert.Equal("†", Decode(reader, 0xA0).Unicode);
+ }
+
+ [Fact]
+ public void NonsymbolicType1_dictionaryWithMacRomanBase_isNotFilledFromStandard()
+ {
+ // §9.6.5.2 states no fill rule for Type1 fonts.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Foo")
+ .Set(new PdfName("FontDescriptor"), NonsymbolicDescriptor())
+ .Set(PdfName.Encoding, MacRomanBaseDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0xB2).Unicode);
+ }
+
+ [Fact]
+ public void SymbolicTrueType_dictionaryWithMacRomanBase_isNotFilledFromStandard()
+ {
+ // §9.6.5.4's table-building paragraph applies to a dictionary /Encoding only when the
+ // Nonsymbolic flag is set.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(4)))
+ .Set(PdfName.Encoding, MacRomanBaseDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0xB2).Unicode);
+ }
+
+ [Theory]
+ [InlineData(36, false)] // Symbolic and Nonsymbolic both set: Symbolic wins, no fill.
+ [InlineData(0, true)] // both clear: Symbolic is clear, so the state is nonsymbolic; fill.
+ public void TrueTypeWithDisagreeingFlags_symbolicFlagDecidesTheFill(int flags, bool filled)
+ {
+ // Table 121 forbids both shapes; §9.8.2 says which flag a processor reads when they occur:
+ // "A PDF processor should always check the Symbolic flag to determine whether the state is
+ // Symbolic or NonSymbolic". The fill follows that, not the Nonsymbolic bit's own value.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(flags));
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), descriptor)
+ .Set(PdfName.Encoding, MacRomanBaseDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(filled ? "†" : null, Decode(reader, 0xB2).Unicode);
+ Assert.Equal("†", Decode(reader, 0xA0).Unicode); // MacRoman's own dagger either way.
+ }
+
+ [Fact]
+ public void NonsymbolicTrueType_dictionaryWithMacExpertBase_isNotFilledFromStandard()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), NonsymbolicDescriptor())
+ .Set(PdfName.Encoding, new PdfDictionary().Set(new PdfName("BaseEncoding"), "MacExpertEncoding"));
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0x41).Unicode);
+ Assert.Null(Decode(reader, 0xB2).Unicode);
+ }
+
+ [Fact]
+ public void TrueTypeWithDescriptorButNoFlags_dictionaryWithMacRomanBase_isNotFilledFromStandard()
+ {
+ // A descriptor without /Flags has no Nonsymbolic flag to be "set" (§9.6.5.4), so the
+ // fill must not run; this is the shape that separates a gate on the resolved /Flags from
+ // a gate on the descriptor's mere presence, which the Table 112 fallback would then read
+ // as nonsymbolic and fill.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), new PdfDictionary())
+ .Set(PdfName.Encoding, MacRomanBaseDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0xB2).Unicode); // not filled.
+ Assert.Equal("†", Decode(reader, 0xA0).Unicode); // MacRoman's own dagger, untouched.
+ }
+
+ [Fact]
+ public void DescriptorlessTrueType_dictionaryWithMacRomanBase_isNotFilledFromStandard()
+ {
+ // §9.6.5.4 conditions the fill on "the font descriptor's Nonsymbolic flag", a flag of a
+ // descriptor that is present: with no /FontDescriptor at all the precondition is not met
+ // and the fill must not run, even though this reader's Table 112 fallback elsewhere
+ // treats a missing descriptor as nonsymbolic.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(PdfName.Encoding, MacRomanBaseDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0xB2).Unicode); // not filled: the twelve cells stay undefined.
+ Assert.Equal("†", Decode(reader, 0xA0).Unicode); // MacRoman's own dagger, untouched.
+ }
+
+ [Fact]
+ public void SymbolicTrueType_namedWinAnsiEncoding_isHonoured_pinning()
+ {
+ // §9.6.5.4, verbatim: "When the font has no Encoding entry, or the font descriptor's
+ // Symbolic flag is set (in which case the Encoding entry is ignored), this shall occur:
+ // ...". This reader does not implement that alternative (it needs a font-program cmap this
+ // reader does not read) and instead honours a present /Encoding even for a symbolic
+ // TrueType font; this pins that departure, not a defect.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(4));
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "TrueType").Set(PdfName.BaseFont, "Foo")
+ .Set(new PdfName("FontDescriptor"), descriptor)
+ .Set(PdfName.Encoding, "WinAnsiEncoding");
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("A", Decode(reader, 0x41).Unicode);
+ }
+
+ // ── 9: Symbol / ZapfDingbats base fonts ──────────────────────────────────────────────────────
+
+ [Fact]
+ public void SymbolBaseFont_noEncoding()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Symbol");
+ var reader = Build(doc, fontDict, sink);
+
+ var alpha = Decode(reader, 0x61);
+ Assert.Equal("α", alpha.Unicode);
+ Assert.Equal(631, alpha.Width);
+
+ Assert.Equal("∀", Decode(reader, 0x22).Unicode); // universal
+ }
+
+ [Fact]
+ public void ZapfDingbatsBaseFont()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("ZapfDingbats");
+ var reader = Build(doc, fontDict, sink);
+
+ var a1 = Decode(reader, 0x21);
+ Assert.Equal("✁", a1.Unicode);
+ Assert.Equal(974, a1.Width);
+
+ var a191 = Decode(reader, 0xFE);
+ Assert.Equal("➾", a191.Unicode);
+ Assert.Equal(918, a191.Width);
+
+ var a89 = Decode(reader, 0x80); // the AFM-only code.
+ Assert.Equal("❨", a89.Unicode);
+ Assert.Equal(390, a89.Width);
+ }
+
+ [Fact]
+ public void SymbolBaseFont_differences_overrideTheBuiltInEncoding()
+ {
+ // Table 112: with no /BaseEncoding, /Differences describes differences from the font's
+ // built-in encoding; §9.6.5.2: an /Encoding entry "shall override a Type 1 font's mapping
+ // from character codes to character names". Symbol is not exempt.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray().Add(new PdfInteger(0x61)).Add(new PdfName("gamma"));
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var reader = Build(doc, Type1("Symbol").Set(PdfName.Encoding, encoding), sink);
+
+ var gamma = Decode(reader, 0x61);
+ Assert.Equal("γ", gamma.Unicode);
+ Assert.Equal(411, gamma.Width); // the AFM width of gamma, not alpha's 631.
+
+ var beta = Decode(reader, 0x62); // untouched by /Differences: still the built-in beta.
+ Assert.Equal("β", beta.Unicode);
+ Assert.Equal(549, beta.Width);
+
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ [Fact]
+ public void SymbolBaseFont_namedWinAnsiEncoding_replacesTheBuiltInEncoding()
+ {
+ // A named /Encoding replaces the whole base table (§9.6.5, Table 112). Symbol's AFM has
+ // no glyph named "a", so the width falls back to MissingWidth, 0 here.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Symbol").Set(PdfName.Encoding, "WinAnsiEncoding");
+ var reader = Build(doc, fontDict, sink);
+
+ var a = Decode(reader, 0x61);
+ Assert.Equal("a", a.Unicode);
+ Assert.Equal(0, a.Width);
+
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ // ── 10: 403 vs 404 ───────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void SymbolicNonStandardFont_noEncodingNoToUnicode_reports403Never404()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var descriptor = new PdfDictionary().Set(new PdfName("Flags"), new PdfInteger(4));
+ var fontDict = Type1("Foo").Set(new PdfName("FontDescriptor"), descriptor);
+ var reader = Build(doc, fontDict, sink);
+
+ Decode(reader, 0x41);
+ Decode(reader, 0x42);
+
+ Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontNoUnicodeRoute);
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.UnmappedGlyphs);
+ }
+
+ [Fact]
+ public void WinAnsiFont_unmappedDifferenceName_reports404OnceOnFirstDecode_never403()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var differences = new PdfArray().Add(new PdfInteger(65)).Add(new PdfName("g123"));
+ var encoding = new PdfDictionary()
+ .Set(new PdfName("BaseEncoding"), new PdfName("WinAnsiEncoding"))
+ .Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Null(Decode(reader, 0x41).Unicode);
+ Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.UnmappedGlyphs);
+
+ Decode(reader, 0x41); // a second decode of the same unmapped code: nothing new reported.
+ Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.UnmappedGlyphs);
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontNoUnicodeRoute);
+ }
+
+ [Fact]
+ public void WinAnsiFont_unmappedDifferenceName_withToUnicode_reportsNeither404Nor403()
+ {
+ // The unparsed /ToUnicode stream may map code 65 (§9.10.2 gives it priority over the
+ // glyph-name route), so a missing glyph-name mapping is not yet an unmapped glyph.
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica "
+ + "/Encoding << /BaseEncoding /WinAnsiEncoding /Differences [65 /g123] >> "
+ + "/ToUnicode 7 0 R >>"),
+ new FontTestSupport.Obj(7, "<< >>", "/CIDInit /ProcSet findresource begin\n"u8.ToArray()));
+ var sink = new DiagnosticSink(50);
+ var reader = Build(doc, Assert.IsType(doc.Resolve(5)), sink);
+
+ Assert.True(reader.HasToUnicode);
+ Assert.Null(Decode(reader, 0x41).Unicode);
+ Assert.Equal("B", Decode(reader, 0x42).Unicode);
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ // ── 11: /Widths ──────────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Widths_explicitArray_missingWidthForOutOfRangeCodes()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var widths = new PdfArray().Add(new PdfInteger(500)).Add(new PdfInteger(600)).Add(new PdfInteger(700));
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(65))
+ .Set(new PdfName("LastChar"), new PdfInteger(67))
+ .Set(new PdfName("Widths"), widths);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(500, Decode(reader, 65).Width);
+ Assert.Equal(600, Decode(reader, 66).Width);
+ Assert.Equal(700, Decode(reader, 67).Width);
+ Assert.Equal(0, Decode(reader, 68).Width);
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ [Fact]
+ public void Widths_missingWidthFromDescriptor()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var widths = new PdfArray().Add(new PdfInteger(500)).Add(new PdfInteger(600)).Add(new PdfInteger(700));
+ var descriptor = new PdfDictionary().Set(new PdfName("MissingWidth"), new PdfInteger(250));
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(65))
+ .Set(new PdfName("LastChar"), new PdfInteger(67))
+ .Set(new PdfName("Widths"), widths)
+ .Set(new PdfName("FontDescriptor"), descriptor);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(250, Decode(reader, 68).Width);
+ }
+
+ [Fact]
+ public void Helvetica_noWidths_descriptorPresentNoFlags_stillFillsAfmWidth()
+ {
+ // The AFM width fill depends only on /Widths being absent and the font resolving to one
+ // of the standard 14; a present /FontDescriptor without /Flags neither enables nor
+ // disables it. The §9.6.5.4 encoding fill is the one that reads /Flags, and
+ // TrueTypeWithDescriptorButNoFlags_dictionaryWithMacRomanBase_isNotFilledFromStandard
+ // pins that side.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(new PdfName("FontDescriptor"), new PdfDictionary());
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(556, Decode(reader, 0xB2).Width); // dagger's Helvetica AFM width, not MissingWidth.
+ }
+
+ [Fact]
+ public void Widths_shortArray_reports402Once_missingWidthForShortfall()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var widths = new PdfArray().Add(new PdfInteger(500));
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(65))
+ .Set(new PdfName("LastChar"), new PdfInteger(67))
+ .Set(new PdfName("Widths"), widths);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(500, Decode(reader, 65).Width);
+ Assert.Equal(0, Decode(reader, 66).Width);
+ Assert.Equal(0, Decode(reader, 67).Width);
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontWidthsMalformed, d.Code);
+ }
+
+ [Fact]
+ public void Widths_nonNumberElement_reports402Once_missingWidthForThatCode()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var widths = new PdfArray().Add(new PdfInteger(500)).Add(new PdfName("x")).Add(new PdfInteger(700));
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(65))
+ .Set(new PdfName("LastChar"), new PdfInteger(67))
+ .Set(new PdfName("Widths"), widths);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(0, Decode(reader, 66).Width);
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontWidthsMalformed, d.Code);
+ }
+
+ [Fact]
+ public void Widths_indirectArrayAndElement_accepted()
+ {
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica "
+ + "/FirstChar 65 /LastChar 65 /Widths 12 0 R >>"),
+ new FontTestSupport.Obj(12, "[13 0 R]"),
+ new FontTestSupport.Obj(13, "999"));
+
+ var sink = new DiagnosticSink(50);
+ var fontDict = (PdfDictionary)doc.Resolve(5)!;
+ var reader = Build(doc, fontDict, sink, objectNumber: 5, generation: 0);
+
+ Assert.Equal(999, Decode(reader, 65).Width);
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ [Fact]
+ public void FirstCharOutOfRange_reports402Once()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var widths = new PdfArray().Add(new PdfInteger(500));
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(300))
+ .Set(new PdfName("LastChar"), new PdfInteger(300))
+ .Set(new PdfName("Widths"), widths);
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontWidthsMalformed, d.Code);
+ }
+
+ [Fact]
+ public void NonStandardFont_noWidths_reports402Once_allMissingWidth()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Foo");
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(0, Decode(reader, 0x41).Width);
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontWidthsMalformed, d.Code);
+ }
+
+ // ── 12: dangling reference, null entries ─────────────────────────────────────────────────────
+
+ [Fact]
+ public void DanglingEncodingReference_treatedAsAbsent_no401()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, new PdfIndirectReference(99, 0));
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("’", Decode(reader, 0x27).Unicode); // falls back to StandardEncoding.
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ }
+
+ [Fact]
+ public void Widths_directNull_treatedAsAbsent_usesAfmWidth_no402()
+ {
+ // ISO 32000-2 §7.3.7: a dictionary entry whose value is null is treated the same as if
+ // the entry does not exist, so this must behave exactly like NonStandardFont's own
+ // /Widths-absent case above, not like a malformed one.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(new PdfName("Widths"), PdfNull.Instance);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal(667, Decode(reader, 0x41).Width);
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontWidthsMalformed);
+ }
+
+ [Fact]
+ public void Widths_referenceToNullObject_treatedAsAbsent_usesAfmWidth_no402()
+ {
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica "
+ + "/Widths 7 0 R >>"),
+ new FontTestSupport.Obj(7, "null"));
+ var sink = new DiagnosticSink(50);
+ var fontDict = (PdfDictionary)doc.Resolve(5)!;
+ var reader = Build(doc, fontDict, sink, objectNumber: 5, generation: 0);
+
+ Assert.Equal(667, Decode(reader, 0x41).Width);
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontWidthsMalformed);
+ }
+
+ [Fact]
+ public void Widths_wrongTypeNotNull_stillReports402()
+ {
+ // The negative control for the two tests above: a present, wrong-typed, non-null /Widths
+ // must still be reported, so the null normalisation is not swallowing malformed entries.
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(65))
+ .Set(new PdfName("LastChar"), new PdfInteger(65))
+ .Set(new PdfName("Widths"), new PdfInteger(5));
+ Build(doc, fontDict, sink);
+
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontWidthsMalformed);
+ Assert.Contains("not an array", d.Message);
+ }
+
+ [Fact]
+ public void Encoding_directNull_treatedAsAbsent_usesStandardEncoding_no401()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, PdfNull.Instance);
+ var reader = Build(doc, fontDict, sink);
+
+ Assert.Equal("’", Decode(reader, 0x27).Unicode); // StandardEncoding's quoteright.
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ }
+
+ [Fact]
+ public void Encoding_referenceToNullObject_treatedAsAbsent_no401()
+ {
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica "
+ + "/Encoding 7 0 R >>"),
+ new FontTestSupport.Obj(7, "null"));
+ var sink = new DiagnosticSink(50);
+ var fontDict = (PdfDictionary)doc.Resolve(5)!;
+ var reader = Build(doc, fontDict, sink, objectNumber: 5, generation: 0);
+
+ Assert.Equal("’", Decode(reader, 0x27).Unicode); // StandardEncoding's quoteright.
+ Assert.DoesNotContain(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ }
+
+ // ── 13: GetFontReader ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void GetFontReader_type0AndType3_returnNull_noDiagnostic()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+
+ var type0 = new PdfDictionary().Set(PdfName.Subtype, "Type0");
+ Assert.Null(doc.GetFontReader(type0, sink, null));
+
+ var type3 = new PdfDictionary().Set(PdfName.Subtype, "Type3");
+ Assert.Null(doc.GetFontReader(type3, sink, null));
+
+ Assert.Empty(sink.Diagnostics);
+ }
+
+ [Fact]
+ public void GetFontReader_unknownSubtype_reports400Once()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var fontDict = new PdfDictionary().Set(PdfName.Subtype, "Foo");
+ Assert.Null(doc.GetFontReader(fontDict, sink, null));
+
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontUnreadable, d.Code);
+ }
+
+ [Fact]
+ public void GetFontReader_notADictionary_reports400Once()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ Assert.Null(doc.GetFontReader(new PdfInteger(1), sink, null));
+
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontUnreadable, d.Code);
+ }
+
+ [Fact]
+ public void GetFontReader_sameIndirectFont_returnsSameInstance()
+ {
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"));
+ var sink = new DiagnosticSink(50);
+
+ var first = doc.GetFontReader(new PdfIndirectReference(5, 0), sink, null);
+ var second = doc.GetFontReader(new PdfIndirectReference(5, 0), sink, null);
+ Assert.Same(first, second);
+ }
+
+ [Fact]
+ public void GetFontReader_twoDirectDictionaries_returnTwoInstances()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var a = doc.GetFontReader(Type1("Helvetica"), sink, null);
+ var b = doc.GetFontReader(Type1("Helvetica"), sink, null);
+ Assert.NotSame(a, b);
+ }
+
+ [Fact]
+ public void GetFontReader_fontEntryNamesADeepLengthChain_reports400Once_noThrow()
+ {
+ // The /Font entry itself (object 3) is a stream whose /Length chains 120 links deep, past
+ // MaxResolveDepth (100). ResolveValue(rawFontEntry) re-enters resolution while parsing that
+ // stream's own structure, so the depth limit throws before the dictionary-type check below
+ // it ever runs; GetFontReader must catch that itself rather than let it escape.
+ var bytes = FontTestSupport.BuildDeepIndirectLengthChain(firstChainObject: 3, chainLen: 120);
+ using var doc = PdfReader.Open(bytes);
+ var sink = new DiagnosticSink(50);
+
+ var result = doc.GetFontReader(new PdfIndirectReference(3, 0), sink, null);
+
+ Assert.Null(result);
+ var d = Assert.Single(sink.Diagnostics);
+ Assert.Equal(PdfReaderDiagnosticCode.FontUnreadable, d.Code);
+ }
+
+ [Fact]
+ public void GetFontReader_disposedReader_throwsObjectDisposedException()
+ {
+ var doc = FontTestSupport.OpenMinimal();
+ doc.Dispose();
+ var sink = new DiagnosticSink(50);
+
+ Assert.Throws(() => doc.GetFontReader(Type1("Helvetica"), sink, null));
+ }
+
+ // ── 14: diagnostics carry object number, generation, page index ─────────────────────────────
+
+ [Fact]
+ public void Diagnostics_carryObjectNumberGenerationAndPageIndex()
+ {
+ using var doc = FontTestSupport.Open(
+ new FontTestSupport.Obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Foo "
+ + "/Encoding /Bogus >>"));
+ var sink = new DiagnosticSink(50);
+ var fontDict = (PdfDictionary)doc.Resolve(5)!;
+ Build(doc, fontDict, sink, objectNumber: 5, generation: 0, pageIndex: 2);
+
+ // This font also has no /Widths, so FontWidthsMalformed fires alongside the pinned
+ // FontEncodingMalformed; both must carry the same object number, generation and page.
+ Assert.NotEmpty(sink.Diagnostics);
+ foreach (var d in sink.Diagnostics)
+ {
+ Assert.Equal(5, d.ObjectNumber);
+ Assert.Equal(0, d.Generation);
+ Assert.Equal(2, d.PageIndex);
+ }
+ }
+
+ // ── 16: allocation bound ─────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Create_allocatesUnder64KiB_forA100000ElementWidthsArray()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+
+ // One PdfInteger instance shared by every slot: the parser has no array-length cap of its
+ // own, so a hostile /Widths can be arbitrarily long, but Create reads at most
+ // LastChar - FirstChar + 1 (here, 1) elements and never copies the array itself.
+ var shared = new PdfInteger(500);
+ var widths = new PdfArray();
+ for (var i = 0; i < 100_000; i++)
+ widths.Add(shared);
+ var fontDict = Type1("Helvetica")
+ .Set(new PdfName("FirstChar"), new PdfInteger(65))
+ .Set(new PdfName("LastChar"), new PdfInteger(65))
+ .Set(new PdfName("Widths"), widths);
+
+ // Warm-up: JIT and any lazy static (AdobeGlyphList's own load) must not be charged to the
+ // measured call.
+ Build(doc, Type1("Helvetica"), new DiagnosticSink(50));
+
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ Build(doc, fontDict, new DiagnosticSink(50));
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+
+ // Measured 6,464 bytes on this runtime (the per-font string/width/Unicode tables, the
+ // ToArray() copies of the shared encoding statics, and the Unicode strings themselves);
+ // 64 KiB is a generous bound that still fails if Create starts copying the
+ // 100,000-element array instead of indexing into it.
+ Assert.True(allocated < 64 * 1024, $"Create allocated {allocated} bytes, expected < 64 KiB.");
+ }
+
+ [Fact]
+ public void Create_allocatesUnder64KiB_forA100000ElementDifferencesArray()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+
+ // 100,000 (code, oversized-name) pairs, alternating over all 256 codes: every element is
+ // read (unlike the /Widths array above, where only LastChar - FirstChar + 1 elements are
+ // read), so this is the array-length cap this class' own comment on that test says the
+ // parser has none of; the 401 message for the first oversized element must not be built
+ // for every later one, only to be discarded by ReportOnce.
+ var oversizedName = new PdfName(new string('a', 129));
+ var differences = new PdfArray();
+ for (var i = 0; i < 100_000; i++)
+ {
+ differences.Add(new PdfInteger(i % 256));
+ differences.Add(oversizedName);
+ }
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ var fontDict = Type1("Helvetica").Set(PdfName.Encoding, encoding);
+
+ // Warm-up: JIT and any lazy static (AdobeGlyphList's own load) must not be charged to the
+ // measured call.
+ Build(doc, Type1("Helvetica"), new DiagnosticSink(50));
+
+ var sink = new DiagnosticSink(50);
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ var reader = Build(doc, fontDict, sink);
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+
+ // Measured 13,336 bytes on this runtime; 64 KiB is the same generous bound the /Widths
+ // KAT above uses.
+ Assert.True(allocated < 64 * 1024, $"Create allocated {allocated} bytes, expected < 64 KiB.");
+ // Every code winds up undefined, so this font also has no Unicode route at all
+ // (FontNoUnicodeRoute, 403), legitimately, alongside the 401 this test pins.
+ Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontEncodingMalformed);
+ for (var code = 0; code < 256; code++)
+ Assert.Null(Decode(reader, (byte)code).Unicode);
+ }
+
+ // ── 17: DiagnosticExcerpt quoting ────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void BaseFontMessage_quotedThroughDiagnosticExcerpt()
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(50);
+ var oneMiBName = new string('B', 1024 * 1024);
+ var fontDict = new PdfDictionary()
+ .Set(PdfName.Subtype, "Type1")
+ .Set(PdfName.BaseFont, oneMiBName);
+ Build(doc, fontDict, sink);
+
+ // No standard 14 font resolves from this oversized name, so FontWidthsMalformed fires
+ // alongside the pinned FontUnreadable.
+ var d = Assert.Single(sink.Diagnostics, d => d.Code == PdfReaderDiagnosticCode.FontUnreadable);
+ Assert.True(d.Message.Length < 200, $"message was {d.Message.Length} characters long.");
+ // DiagnosticExcerpt.Quote's exact shape: the first 32 characters, an ellipsis, and the
+ // decoded value's own byte length in parentheses.
+ Assert.Contains(new string('B', 32) + "... (1048576 bytes)", d.Message);
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs
new file mode 100644
index 00000000..aba94468
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs
@@ -0,0 +1,76 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+public sealed class Standard14NamesTests
+{
+ [Theory]
+ [InlineData("Helvetica")]
+ [InlineData("Helvetica-Bold")]
+ [InlineData("Helvetica-Oblique")]
+ [InlineData("Helvetica-BoldOblique")]
+ [InlineData("Times-Roman")]
+ [InlineData("Times-Bold")]
+ [InlineData("Times-Italic")]
+ [InlineData("Times-BoldItalic")]
+ [InlineData("Courier")]
+ [InlineData("Courier-Bold")]
+ [InlineData("Courier-Oblique")]
+ [InlineData("Courier-BoldOblique")]
+ [InlineData("Symbol")]
+ [InlineData("ZapfDingbats")]
+ public void TryResolve_theFourteenExactNames(string name)
+ {
+ Assert.True(Standard14Names.TryResolve(name, out var afmName));
+ Assert.Equal(name, afmName);
+ }
+
+ [Fact]
+ public void TryResolve_stripsSubsetTag()
+ {
+ Assert.True(Standard14Names.TryResolve("ABCDEF+Helvetica", out var afmName));
+ Assert.Equal("Helvetica", afmName);
+ }
+
+ [Fact]
+ public void TryResolve_arialBold_toHelveticaBold()
+ {
+ Assert.True(Standard14Names.TryResolve("Arial,Bold", out var afmName));
+ Assert.Equal("Helvetica-Bold", afmName);
+ }
+
+ [Fact]
+ public void TryResolve_timesNewRomanPSBoldItalicMT_toTimesBoldItalic()
+ {
+ Assert.True(Standard14Names.TryResolve("TimesNewRomanPS-BoldItalicMT", out var afmName));
+ Assert.Equal("Times-BoldItalic", afmName);
+ }
+
+ [Fact]
+ public void TryResolve_courierNew_toCourier()
+ {
+ Assert.True(Standard14Names.TryResolve("CourierNew", out var afmName));
+ Assert.Equal("Courier", afmName);
+ }
+
+ [Fact]
+ public void TryResolve_isCaseSensitive()
+ {
+ Assert.False(Standard14Names.TryResolve("arial", out _));
+ }
+
+ [Fact]
+ public void TryResolve_helveticaNarrow_false()
+ {
+ Assert.False(Standard14Names.TryResolve("Helvetica-Narrow", out _));
+ }
+
+ [Fact]
+ public void TryResolve_200CharacterName_false()
+ {
+ Assert.False(Standard14Names.TryResolve(new string('A', 200), out _));
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs
new file mode 100644
index 00000000..36c44ed1
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs
@@ -0,0 +1,195 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+using VellumPdf.Core;
+using VellumPdf.Fonts;
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+///
+/// Pins ' generated widths against the AFM files (every number
+/// here was read from the AFM file directly, via grep N <name> ;, not from the
+/// generated file), for the two symbolic fonts and, through
+/// and a live ,
+/// the twelve nonsymbolic text fonts' own name-keyed width tables.
+///
+public sealed class SymbolFontMetricsTests
+{
+ [Fact]
+ public void SymbolWidths_pinnedEntries()
+ {
+ Assert.Equal(631, SymbolFontMetrics.SymbolWidths["alpha"]);
+ Assert.Equal(250, SymbolFontMetrics.SymbolWidths["space"]);
+ Assert.Equal(190, SymbolFontMetrics.SymbolWidths.Count);
+ Assert.True(SymbolFontMetrics.SymbolWidths.ContainsKey("apple"));
+ }
+
+ [Fact]
+ public void ZapfDingbatsWidths_pinnedEntries()
+ {
+ Assert.Equal(974, SymbolFontMetrics.ZapfDingbatsWidths["a1"]);
+ Assert.Equal(278, SymbolFontMetrics.ZapfDingbatsWidths["space"]);
+ Assert.Equal(390, SymbolFontMetrics.ZapfDingbatsWidths["a89"]);
+ Assert.Equal(918, SymbolFontMetrics.ZapfDingbatsWidths["a191"]);
+ Assert.Equal(202, SymbolFontMetrics.ZapfDingbatsWidths.Count);
+ }
+
+ // ── Text-font width tables: name-keyed, no WinAnsi dependency ───────────────────────────────
+ // "lslash" and "fraction" have no WinAnsi code point; each font's own AFM WX for both, plus
+ // for "A" (inside WinAnsi), pins that the lookup is by glyph name, so a name outside WinAnsi
+ // cannot fall back to another glyph's width.
+
+ [Theory]
+ [InlineData("Helvetica", "A", 667)]
+ [InlineData("Helvetica", "fraction", 167)]
+ [InlineData("Helvetica", "lslash", 222)]
+ [InlineData("Helvetica-Bold", "A", 722)]
+ [InlineData("Helvetica-Bold", "fraction", 167)]
+ [InlineData("Helvetica-Bold", "lslash", 278)]
+ [InlineData("Helvetica-Oblique", "A", 667)]
+ [InlineData("Helvetica-Oblique", "fraction", 167)]
+ [InlineData("Helvetica-Oblique", "lslash", 222)]
+ [InlineData("Helvetica-BoldOblique", "A", 722)]
+ [InlineData("Helvetica-BoldOblique", "fraction", 167)]
+ [InlineData("Helvetica-BoldOblique", "lslash", 278)]
+ [InlineData("Times-Roman", "A", 722)]
+ [InlineData("Times-Roman", "fraction", 167)]
+ [InlineData("Times-Roman", "lslash", 278)]
+ [InlineData("Times-Bold", "A", 722)]
+ [InlineData("Times-Bold", "fraction", 167)]
+ [InlineData("Times-Bold", "lslash", 278)]
+ [InlineData("Times-Italic", "A", 611)]
+ [InlineData("Times-Italic", "fraction", 167)]
+ [InlineData("Times-Italic", "lslash", 278)]
+ [InlineData("Times-BoldItalic", "A", 667)]
+ [InlineData("Times-BoldItalic", "fraction", 167)]
+ [InlineData("Times-BoldItalic", "lslash", 278)]
+ [InlineData("Courier", "A", 600)]
+ [InlineData("Courier", "fraction", 600)]
+ [InlineData("Courier", "lslash", 600)]
+ [InlineData("Courier-Bold", "A", 600)]
+ [InlineData("Courier-Bold", "fraction", 600)]
+ [InlineData("Courier-Bold", "lslash", 600)]
+ [InlineData("Courier-Oblique", "A", 600)]
+ [InlineData("Courier-Oblique", "fraction", 600)]
+ [InlineData("Courier-Oblique", "lslash", 600)]
+ [InlineData("Courier-BoldOblique", "A", 600)]
+ [InlineData("Courier-BoldOblique", "fraction", 600)]
+ [InlineData("Courier-BoldOblique", "lslash", 600)]
+ public void TextFontWidths_pinnedAgainstAfm(string afmName, string glyphName, int width)
+ {
+ Assert.True(SymbolFontMetrics.TryGetTextFontWidths(afmName, out var widths));
+ Assert.Equal(width, widths[glyphName]);
+ }
+
+ [Fact]
+ public void TextFontWidths_unknownAfmName_returnsFalse()
+ {
+ Assert.False(SymbolFontMetrics.TryGetTextFontWidths("Symbol", out _));
+ Assert.False(SymbolFontMetrics.TryGetTextFontWidths("Helvetica-Narrow", out _));
+ }
+
+ // ── Standard 14 width route, through SimpleFontReader ───────────────────────────────────────
+
+ private static PdfDictionary FontDict(string baseFont, PdfArray? differences = null)
+ {
+ var dict = new PdfDictionary()
+ .Set(PdfName.Subtype, "Type1")
+ .Set(PdfName.BaseFont, baseFont);
+ if (differences is not null)
+ {
+ var encoding = new PdfDictionary().Set(new PdfName("Differences"), differences);
+ dict.Set(PdfName.Encoding, encoding);
+ }
+ return dict;
+ }
+
+ private static PdfFontReader Build(PdfDictionary fontDict)
+ {
+ using var doc = FontTestSupport.OpenMinimal();
+ var sink = new DiagnosticSink(cap: 50);
+ return SimpleFontReader.Create(doc, fontDict, objectNumber: null, generation: null, sink, pageIndex: null);
+ }
+
+ private static double WidthOf(PdfFontReader reader, byte code)
+ {
+ ReadOnlySpan bytes = [code];
+ var offset = 0;
+ Assert.True(reader.TryDecodeNext(bytes, ref offset, out var glyph));
+ return glyph.Width;
+ }
+
+ [Fact]
+ public void Helvetica_codeA_width667()
+ {
+ var reader = Build(FontDict("Helvetica"));
+ Assert.Equal(667, WidthOf(reader, 0x41));
+ }
+
+ [Fact]
+ public void Helvetica_codeSpace_width278()
+ {
+ var reader = Build(FontDict("Helvetica"));
+ Assert.Equal(278, WidthOf(reader, 0x20));
+ }
+
+ [Fact]
+ public void FiOutsideWinAnsi_measuresAsItsOwnAfmWidth_500()
+ {
+ // "fi" has no WinAnsi code point; a code-point-keyed lookup would fall back to another
+ // glyph's width (Helvetica's "?" is 556). The name-keyed table measures Helvetica.afm's
+ // own "fi" at 500.
+ var differences = new PdfArray().Add(new PdfInteger(65)).Add(new PdfName("fi"));
+ var reader = Build(FontDict("Helvetica", differences));
+ Assert.Equal(500, WidthOf(reader, 0x41));
+ }
+
+ [Fact]
+ public void TimesRoman_codeA_width722()
+ {
+ var reader = Build(FontDict("Times-Roman"));
+ Assert.Equal(722, WidthOf(reader, 0x41));
+ }
+
+ [Fact]
+ public void Courier_anyCode_width600()
+ {
+ var reader = Build(FontDict("Courier"));
+ Assert.Equal(600, WidthOf(reader, 0x41));
+ Assert.Equal(600, WidthOf(reader, 0x7A));
+ }
+
+ // ── Transcription cross-check: Reader WinAnsi vs Kernel WinAnsiEncoding ─────────────────────
+
+ [Fact]
+ public void WinAnsi_roundTripsThroughKernelEncoding_for216Codes()
+ {
+ var table = SimpleFontEncodings.WinAnsi;
+ var checkedCount = 0;
+ for (var code = 0x20; code <= 0xFF; code++)
+ {
+ if (code is 0x7F or 0x81 or 0x8D or 0x8F or 0x90 or 0x9D or 0xA0 or 0xAD)
+ continue; // the six bullet fills, plus 0xA0/0xAD, are checked separately below.
+
+ var name = table[code];
+ Assert.NotNull(name);
+ Assert.True(AdobeGlyphList.TryMapToUnicode(name!, out var unicode));
+ Assert.Equal(1, unicode.Length);
+ Assert.True(WinAnsiEncoding.TryGetByte(unicode[0], out var b));
+ Assert.Equal((byte)code, b);
+ checkedCount++;
+ }
+ Assert.Equal(216, checkedCount);
+ }
+
+ [Fact]
+ public void WinAnsi_footnoteCodes_mapToTheirLiteralCodepoints()
+ {
+ Assert.True(AdobeGlyphList.TryMapToUnicode(SimpleFontEncodings.WinAnsi[0xA0]!, out var space));
+ Assert.Equal(" ", space);
+ Assert.True(AdobeGlyphList.TryMapToUnicode(SimpleFontEncodings.WinAnsi[0xAD]!, out var hyphen));
+ Assert.Equal("-", hyphen);
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs
new file mode 100644
index 00000000..fc85b76a
--- /dev/null
+++ b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs
@@ -0,0 +1,62 @@
+// Copyright © Timothy van der Ham (@Tim81)
+// SPDX-License-Identifier: Apache-2.0
+
+using VellumPdf.Reader.Fonts;
+
+namespace VellumPdf.Reader.Tests.Fonts;
+
+/// Pins against the bundled
+/// zapfdingbats.txt data.
+public sealed class ZapfDingbatsGlyphListTests
+{
+ [Theory]
+ [InlineData("a1", "✁")]
+ [InlineData("a89", "❨")]
+ // U+27BE, not U+275E: the two differ by one hex digit, so a transcription slip would be
+ // easy to miss.
+ [InlineData("a191", "➾")]
+ public void TryMap_pinnedEntries(string name, string expected)
+ {
+ Assert.True(ZapfDingbatsGlyphList.TryMap(name, out var unicode));
+ Assert.Equal(expected, unicode);
+ }
+
+ [Fact]
+ public void TryMap_space_false()
+ {
+ // The bundled zapfdingbats.txt itself has no "space" entry (every one of its 201 lines is
+ // an "aNN" name); a font's own space code still gets U+0020 through
+ // SimpleFontReader's fallback to AdobeGlyphList, which does list "space".
+ Assert.False(ZapfDingbatsGlyphList.TryMap("space", out _));
+ }
+
+ [Fact]
+ public void TryMap_unknownName_false()
+ {
+ Assert.False(ZapfDingbatsGlyphList.TryMap("a999", out _));
+ }
+
+ [Fact]
+ public void TryMap_unknownName_outParamIsEmptyStringNotNull()
+ {
+ // The out parameter is declared non-nullable; an unknown name must not hand the caller a
+ // null string through it.
+ Assert.False(ZapfDingbatsGlyphList.TryMap("a999", out var unicode));
+ Assert.Equal("", unicode);
+ }
+
+ [Fact]
+ public void EntryCount_is201()
+ {
+ // Not 188: the committed ZapfDingbatsGlyphList.txt is the Adobe AGL repository's own
+ // zapfdingbats.txt normalised verbatim, and that file maps all 202 ZapfDingbats.afm glyph
+ // names except "space" (which needs no lookup), including the 14 names assigned to codes
+ // 0x80 through 0x8D (a85 through a96, a205, a206), which SymbolFontMetrics' own remarks
+ // describe as ZapfDingbats.afm-only and Annex D.6 does not document. Those 14 names carry
+ // ordinary Unicode mappings in Adobe's own zapfdingbats.txt, not in the Adobe Glyph List
+ // proper. Trimming the list to the 188 names Annex D.6 documents would leave 0x80 ("a89")
+ // with no Unicode route at all, contradicting the KAT SimpleFontReaderTests pins for that
+ // exact code.
+ Assert.Equal(201, ZapfDingbatsGlyphList.Count);
+ }
+}
diff --git a/tests/VellumPdf.Reader.Tests/PdfReaderDiagnosticCodeTests.cs b/tests/VellumPdf.Reader.Tests/PdfReaderDiagnosticCodeTests.cs
index bba71d2c..59620992 100644
--- a/tests/VellumPdf.Reader.Tests/PdfReaderDiagnosticCodeTests.cs
+++ b/tests/VellumPdf.Reader.Tests/PdfReaderDiagnosticCodeTests.cs
@@ -46,6 +46,11 @@ public sealed class PdfReaderDiagnosticCodeTests
[PdfReaderDiagnosticCode.InlineImageMalformed] = 3,
[PdfReaderDiagnosticCode.ContentStreamTooLarge] = 3,
[PdfReaderDiagnosticCode.ContentLimitExceeded] = 3,
+ [PdfReaderDiagnosticCode.FontUnreadable] = 4,
+ [PdfReaderDiagnosticCode.FontEncodingMalformed] = 4,
+ [PdfReaderDiagnosticCode.FontWidthsMalformed] = 4,
+ [PdfReaderDiagnosticCode.FontNoUnicodeRoute] = 4,
+ [PdfReaderDiagnosticCode.UnmappedGlyphs] = 4,
[PdfReaderDiagnosticCode.DiagnosticsSuppressed] = 9,
};
@@ -154,6 +159,11 @@ private static PdfReaderDiagnostic MakeDiagnostic(PdfReaderDiagnosticCode code)
[PdfReaderDiagnosticCode.InlineImageMalformed] = (307, PdfReaderDiagnosticSeverity.Warning),
[PdfReaderDiagnosticCode.ContentStreamTooLarge] = (308, PdfReaderDiagnosticSeverity.Warning),
[PdfReaderDiagnosticCode.ContentLimitExceeded] = (309, PdfReaderDiagnosticSeverity.Warning),
+ [PdfReaderDiagnosticCode.FontUnreadable] = (400, PdfReaderDiagnosticSeverity.Warning),
+ [PdfReaderDiagnosticCode.FontEncodingMalformed] = (401, PdfReaderDiagnosticSeverity.Warning),
+ [PdfReaderDiagnosticCode.FontWidthsMalformed] = (402, PdfReaderDiagnosticSeverity.Warning),
+ [PdfReaderDiagnosticCode.FontNoUnicodeRoute] = (403, PdfReaderDiagnosticSeverity.Info),
+ [PdfReaderDiagnosticCode.UnmappedGlyphs] = (404, PdfReaderDiagnosticSeverity.Info),
[PdfReaderDiagnosticCode.DiagnosticsSuppressed] = (900, PdfReaderDiagnosticSeverity.Warning),
};