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), };