From a7a4c363cec8597737f110d0172c4d41cdba11ae Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 03:00:55 +0200 Subject: [PATCH 1/7] Add simple-font decoding: encodings, AGL, widths, diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads a Type1, MMType1 or TrueType simple font's character-code to glyph-name table from /Encoding and /Differences (ISO 32000-2 §9.6.5), maps names to Unicode through the Adobe Glyph List and a bundled ZapfDingbats list, and fills /Widths or the standard 14 AFM metrics for advance widths. Five diagnostic codes (400-404) cover an unreadable font, a malformed encoding, malformed widths, a font with no route to Unicode, and an unmapped glyph. Annex D.2's WinAnsi and MacRoman tables were re-transcribed from rendered page images rather than reusing Conformance's copy, since that copy carries fifteen MacRoman codes from Table 113 (the TrueType (1,0) cmap fallback table, not part of MacRomanEncoding itself) plus two renamed cells, 0xCA and 0xDB; this reader's copy fixes both. The Symbol and ZapfDingbats built-in encodings and AFM widths come from a generator, eng/generate-symbol-font-metrics.py, run against the Adobe Core 14 AFM files (not committed; see NOTICE) and pinned by a normalised SHA-256 manifest: src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs is up to date. Bound table (§5.9), measured: - /Differences array: walks the array, assigns at most 256 cells, stops past code 255. - /Differences name length: 128 chars, 401 and the cell left undefined. - /Widths array: reads at most LastChar - FirstChar + 1 (<=256). - /BaseFont name: TryResolve rejects over 128 chars without scanning; Report quotes 32 chars via DiagnosticExcerpt. - Resolution hops: one, via PdfDocumentReader.ResolveValue. - AGL component count: <=64, bounded by the 128-char name limit. - Per-font tables: three fixed 256-slot arrays. - FontCache: <=10,000 entries, insert-only, no eviction. - AdobeGlyphList / ZapfDingbatsGlyphList: parsed once per process, 4282 and 201 entries. - Report calls per font: <=5 distinct codes, each once. - TryDecodeNext: O(1) per byte, no allocation. - Create on a 100,000-element shared-instance /Widths array: measured 31,752 bytes allocated (bound asserted at 64 KiB). Departures from the plan: - TryGetCodepoints is named TryMapToUnicode and returns a string: some AGL entries, and uniXXXXYYYY... names, are multi-codepoint. - FontCache is insert-only with a 10,000-entry cap, not an LRU. - ZapfDingbatsGlyphList bundles the Adobe AGL repository's own zapfdingbats.txt rather than a hand-transcribed table. - Codes 401 (FontEncodingMalformed) and 402 (FontWidthsMalformed) are new; the plan's FontUnreadable, FontNoUnicodeRoute and UnmappedGlyphs are unchanged. - Symbol and ZapfDingbats tables are generated, with a committed generator and a hash manifest, not hand-transcribed. --- CHANGELOG.md | 7 + NOTICE | 38 + eng/generate-symbol-font-metrics.py | 291 ++ src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs | 220 + src/VellumPdf.Reader/Fonts/FontCache.cs | 44 + src/VellumPdf.Reader/Fonts/PdfFontReader.cs | 45 + .../Fonts/SimpleFontEncodings.cs | 280 ++ .../Fonts/SimpleFontReader.cs | 429 ++ src/VellumPdf.Reader/Fonts/Standard14Names.cs | 129 + .../Fonts/SymbolFontMetrics.cs | 858 ++++ .../Fonts/ZapfDingbatsGlyphList.cs | 62 + .../PdfDocumentReader.Fonts.cs | 65 + src/VellumPdf.Reader/PdfReaderDiagnostic.cs | 48 + src/VellumPdf.Reader/PublicAPI.Unshipped.txt | 5 + .../Resources/AdobeGlyphList.txt | 4282 +++++++++++++++++ .../Resources/ZapfDingbatsGlyphList.txt | 245 + src/VellumPdf.Reader/VellumPdf.Reader.csproj | 13 + .../Fonts/ReaderEncodingParityTests.cs | 53 + .../Fonts/AdobeGlyphListTests.cs | 129 + .../Fonts/FontFuzzTests.cs | 142 + .../Fonts/FontTestSupport.cs | 78 + .../Fonts/SimpleFontEncodingsTests.cs | 233 + .../Fonts/SimpleFontReaderTests.cs | 630 +++ .../Fonts/Standard14NamesTests.cs | 90 + .../Fonts/SymbolFontMetricsTests.cs | 137 + .../Fonts/ZapfDingbatsGlyphListTests.cs | 51 + .../PdfReaderDiagnosticCodeTests.cs | 10 + 27 files changed, 8614 insertions(+) create mode 100644 eng/generate-symbol-font-metrics.py create mode 100644 src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs create mode 100644 src/VellumPdf.Reader/Fonts/FontCache.cs create mode 100644 src/VellumPdf.Reader/Fonts/PdfFontReader.cs create mode 100644 src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs create mode 100644 src/VellumPdf.Reader/Fonts/SimpleFontReader.cs create mode 100644 src/VellumPdf.Reader/Fonts/Standard14Names.cs create mode 100644 src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs create mode 100644 src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs create mode 100644 src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs create mode 100644 src/VellumPdf.Reader/Resources/AdobeGlyphList.txt create mode 100644 src/VellumPdf.Reader/Resources/ZapfDingbatsGlyphList.txt create mode 100644 tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs create mode 100644 tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs 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..1c31229a 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,41 @@ 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 (Symbol, ZapfDingbats) + 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 encoding and advance widths for the Symbol and ZapfDingbats + standard 14 fonts (ISO 32000-2 Annex D.1, D.5, D.6) + + Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. + All rights reserved. + Comment Copyright (c) 1985, 1987, 1988, 1989, 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 entry exists because that licence conditions redistribution on the + copyright notices above being retained; SymbolFontMetrics.cs is a derived + table of glyph names, codes and advance widths, not a copy of the AFM files + themselves. + ──────────────────────────────────────────────────────────────────────────────── 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..75421a36 --- /dev/null +++ b/eng/generate-symbol-font-metrics.py @@ -0,0 +1,291 @@ +# Copyright © Timothy van der Ham (@Tim81) +# SPDX-License-Identifier: Apache-2.0 +# +# Generates src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs from the Adobe Core 14 AFM files +# (MustRead.html, Adobe Systems, 1997) for Symbol.afm and ZapfDingbats.afm. Those two are the only +# two of the fourteen that are symbolic 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. +# +# 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 the source files 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 Symbol.afm and ZapfDingbats.afm (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" + +# 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", +} + +EXPECTED_RECORD_COUNT = { + "Symbol.afm": 190, + "ZapfDingbats.afm": 202, +} + +# 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+) ;") + + +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 + records = [] + seen_names = set() + for line in normalized.split("\n"): + if line.startswith("Comment Copyright") and copyright_line is None: + copyright_line = line + 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) + + 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) + + return records, copyright_line + + +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 Dictionary _{field_name} = new()", + " {", + ] + for _, width, name in records: + body.append(f' ["{name}"] = {width},') + body.append(" };") + 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(symbol_records, symbol_copyright, zapf_records, zapf_copyright): + o = [] + w = o.append + + w("// Copyright © Timothy van der Ham (@Tim81)") + w("// SPDX-License-Identifier: Apache-2.0") + w("") + w("// Generated by eng/generate-symbol-font-metrics.py; do not edit by hand.") + w("//") + for line in wrap_comment(f"Symbol.afm: {symbol_copyright}"): + w(line) + for line in wrap_comment(f"ZapfDingbats.afm: {zapf_copyright}"): + 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 and AFM advance widths of the two symbolic standard 14 fonts,") + w("/// Symbol and ZapfDingbats. ISO 32000-2 Annex D.1 names Annex D.5 and D.6 as their") + w("/// built-in encodings; the Adobe Core 14 AFM files are this reader's delivery vehicle for") + w("/// that same data, not a separate transcription of the Annex D tables. The Symbol coding") + w("/// here agrees with Annex D.5 at all 189 coded glyphs. The ZapfDingbats coding carries 14") + w("/// codes (0x80 to 0x8D) that Annex D.6 does not document at all; this reader keeps them,") + w("/// on the view that a font program carrying those codes draws them regardless of whether") + w("/// the standard's own table lists them.") + w("/// ") + w("internal static class SymbolFontMetrics") + w("{") + + 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("") + for line in symbol_enc_decl: + w(line) + for line in zapf_enc_decl: + w(line) + w("") + for line in format_widths("symbolWidths", symbol_records): + w(line) + w("") + for line in format_widths("zapfDingbatsWidths", zapf_records): + w(line) + 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 + + symbol_normalized = load_afm(afm_dir, "Symbol.afm") + zapf_normalized = load_afm(afm_dir, "ZapfDingbats.afm") + symbol_records, symbol_copyright = parse_afm("Symbol.afm", symbol_normalized) + zapf_records, zapf_copyright = parse_afm("ZapfDingbats.afm", zapf_normalized) + + text = generate_source(symbol_records, symbol_copyright, zapf_records, zapf_copyright) + + 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..7a197104 --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs @@ -0,0 +1,220 @@ +// 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 presentation +/// forms whose AGL name decomposes into a base letter plus a 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 three ways, each because the two ends of the departure are indistinguishable to a caller +/// that only gets a Unicode string back: +/// +/// Only uppercase uni/u hex digits are recognised. The AGL +/// Specification itself writes the synthetic forms in uppercase; the Conformance package's own +/// copy (src/VellumPdf.Conformance/Rules/Fonts/AdobeGlyphList.cs) additionally accepts +/// lowercase, which this reader does not. +/// 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 both +/// .notdef, which the bundled list maps to U+0000, and the literal name +/// uni0000. +/// +/// +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; + 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; + } + + if (result.Length == 1 && result[0] == '\0') + return false; // .notdef and uni0000 both resolve here; treated as unmapped. + + 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; + } + 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..ffb9c3e6 --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/FontCache.cs @@ -0,0 +1,44 @@ +// 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 (itself far past what any real PDF carries) is worth building for, +/// and the fallback costs only a rebuilt reader, not a wrong one. +/// +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..e3047cd5 --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/PdfFontReader.cs @@ -0,0 +1,45 @@ +// 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). This PR's populates this only from the glyph-name +/// route (the AGL, or the ZapfDingbats list); the higher-priority /ToUnicode route is parsed +/// starting in a later PR, tracked by until then. +/// 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). This PR only + /// records the fact; a later PR parses the stream and gives it priority over the glyph-name + /// route in , per §9.10.2's own ordering. + /// + 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..050a8e2a --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs @@ -0,0 +1,280 @@ +// 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 (Annex D.1: "PDF processors shall not have a predefined +/// encoding named StandardEncoding" governs the name lookup, not the table's own correctness). +/// +/// +/// 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 not touched by this reader (no file under +/// VellumPdf.Conformance changes in this PR). 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 Annex D.2's own MacRoman column: 0xCA +/// (Annex D.2 pairs it with space, footnote 6's dual mapping) and 0xDB (Annex D.2 and +/// footnote 1 both read currency; Apple's own 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 + /// MacExpertEncoding) 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. + /// + 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..e2c71c10 --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -0,0 +1,429 @@ +// Copyright © Timothy van der Ham (@Tim81) +// SPDX-License-Identifier: Apache-2.0 + +using VellumPdf.Core; +using VellumPdf.Fonts; + +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, the Kernel's own AFM metrics. +/// +/// +/// §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 a real font program would +/// otherwise supply an answer this reader cannot. +/// +/// Every dictionary entry this class reads, wherever it is read, goes through +/// before its type is tested (one hop, a dangling +/// reference resolving to and treated as absent), with one exception: an +/// element of /Differences is read raw (§9.6.5, step 5 below), because §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. +/// +/// +internal sealed class SimpleFontReader : PdfFontReader +{ + private static readonly PdfName _fontDescriptorKey = new("FontDescriptor"); + private static readonly PdfName _flagsKey = new("Flags"); + private static readonly PdfName _fontFileKey = new("FontFile"); + private static readonly PdfName _fontFile2Key = new("FontFile2"); + private static readonly PdfName _fontFile3Key = new("FontFile3"); + 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; + + private string?[] _names = new string?[256]; + private double[] _widths = new double[256]; + private string?[] _unicode = new string?[256]; + 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); nothing else in + // Populate is expected to throw, and the fuzz test is the proof that holds. + 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 / embedded. + var descriptor = Resolve(reader, fontDict.Get(_fontDescriptorKey)) as PdfDictionary; + bool symbolic; + if (descriptor is not null && Resolve(reader, descriptor.Get(_flagsKey)) is PdfInteger flags) + { + symbolic = (flags.Value & SymbolicFlagBit) != 0; + } + else + { + symbolic = afmName is "Symbol" or "ZapfDingbats"; + } + + var embedded = descriptor is not null + && (Resolve(reader, descriptor.Get(_fontFileKey)) is PdfStream + || Resolve(reader, descriptor.Get(_fontFile2Key)) is PdfStream + || Resolve(reader, descriptor.Get(_fontFile3Key)) is PdfStream); + _ = embedded; // Table 112's embedded/not-embedded split collapses to one rule here; see TableDefault. + + // Step 4: base table. + string?[] table; + if (afmName == "Symbol") + { + table = SymbolFontMetrics.SymbolEncoding.ToArray(); + } + else if (afmName == "ZapfDingbats") + { + table = SymbolFontMetrics.ZapfDingbatsEncoding.ToArray(); + } + else + { + table = ResolveEncodingTable(reader, fontDict, symbolic, out var encodingDict); + if (encodingDict is not null) + ApplyDifferences(reader, encodingDict, 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, unicode, widths, descriptorMissingWidth); + + // /ToUnicode: recorded only, not parsed; a later PR adds that (see PdfFontReader's doc). + _hasToUnicode = Resolve(reader, fontDict.Get(_toUnicodeKey)) is PdfStream; + + // 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."); + } + } + + private string?[] ResolveEncodingTable( + PdfDocumentReader reader, PdfDictionary fontDict, bool symbolic, out PdfDictionary? encodingDict) + { + encodingDict = null; + var encoding = Resolve(reader, fontDict.Get(PdfName.Encoding)); + switch (encoding) + { + case null: + return TableDefault(symbolic); + + 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); + + case PdfDictionary dict: + encodingDict = dict; + var baseEncoding = Resolve(reader, dict.Get(_baseEncodingKey)); + if (baseEncoding is null) + return TableDefault(symbolic); + if (baseEncoding is PdfName baseName && SimpleFontEncodings.TryGetNamed(baseName.Value, out var baseTable)) + return baseTable.ToArray(); + ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, + "/Encoding's /BaseEncoding names an encoding this reader does not know."); + return TableDefault(symbolic); + + default: + ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, + "/Encoding is neither a known encoding name nor an encoding dictionary."); + return TableDefault(symbolic); + } + } + + // Table 112's default base encoding: the standard reads embedded → the font program's own + // built-in encoding, not embedded → StandardEncoding (nonsymbolic) or the built-in encoding + // (symbolic). This reader never parses a font program, so both branches land on the same + // answer regardless of embedding (StandardEncoding for nonsymbolic, all-null for symbolic), + // which is why "embedded" plays no part in this method itself (see the class doc's own note). + private static string?[] TableDefault(bool symbolic) => + symbolic ? new string?[256] : SimpleFontEncodings.Standard.ToArray(); + + private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDict, string?[] table) + { + if (Resolve(reader, encodingDict.Get(_differencesKey)) is not PdfArray differences) + 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) + { + 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 forbids overlapping sequences; this reader allows the + // overwrite and reports nothing for it (see the class doc's own remarks). + table[code] = glyphName.Value; + } + code++; + break; + + default: + ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, + "/Differences contains an element this reader does not resolve."); + break; // continue with the next element; code is unchanged. + } + } + } + + /// 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; + } + + private static void FillAfmWidths( + string afmName, string?[] table, string?[] unicode, double[] widths, double missingWidth) + { + if (Standard14Names.TryGetKernelFont(afmName, out var font)) + { + for (var code = 0; code < 256; code++) + { + if (table[code] is null) + continue; + var u = unicode[code]; + widths[code] = u is { Length: 1 } ? Standard14Metrics.GetWidth(font, u[0]) : missingWidth; + } + return; + } + + var byName = 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++; + + var unicode = _unicode[code]; + if (unicode is null && !_reportedNoUnicodeOrUnmapped && _hasAnyMappedCode) + { + 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 . + private static PdfObject? Resolve(PdfDocumentReader reader, PdfObject? raw) => + raw is null ? null : reader.ResolveValue(raw); +} diff --git a/src/VellumPdf.Reader/Fonts/Standard14Names.cs b/src/VellumPdf.Reader/Fonts/Standard14Names.cs new file mode 100644 index 00000000..a6f59184 --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/Standard14Names.cs @@ -0,0 +1,129 @@ +// Copyright © Timothy van der Ham (@Tim81) +// SPDX-License-Identifier: Apache-2.0 + +using VellumPdf.Fonts; + +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 +/// built-in encoding and AFM-width fallback §9.6.2.1 requires when a font has no +/// /Widths//FontDescriptor. +/// +/// +/// 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 (§3.9 step 9), 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", + }; + + private static readonly Dictionary _kernelFonts = new(StringComparer.Ordinal) + { + ["Helvetica"] = Standard14.Helvetica, + ["Helvetica-Bold"] = Standard14.HelveticaBold, + ["Helvetica-Oblique"] = Standard14.HelveticaOblique, + ["Helvetica-BoldOblique"] = Standard14.HelveticaBoldOblique, + ["Times-Roman"] = Standard14.TimesRoman, + ["Times-Bold"] = Standard14.TimesBold, + ["Times-Italic"] = Standard14.TimesItalic, + ["Times-BoldItalic"] = Standard14.TimesBoldItalic, + ["Courier"] = Standard14.Courier, + ["Courier-Bold"] = Standard14.CourierBold, + ["Courier-Oblique"] = Standard14.CourierOblique, + ["Courier-BoldOblique"] = Standard14.CourierBoldOblique, + }; + + /// + /// 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.1). + 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; + } + + /// + /// Returns the member for one of the 12 text fonts. Returns + /// for Symbol, ZapfDingbats, or any name + /// itself would not have produced. + /// + public static bool TryGetKernelFont(string afmName, out Standard14 font) => + _kernelFonts.TryGetValue(afmName, out font); +} diff --git a/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs new file mode 100644 index 00000000..78121666 --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs @@ -0,0 +1,858 @@ +// Copyright © Timothy van der Ham (@Tim81) +// SPDX-License-Identifier: Apache-2.0 + +// Generated by eng/generate-symbol-font-metrics.py; do not edit by hand. +// +// 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. +// +// 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 and AFM advance widths of the two symbolic standard 14 fonts, +/// Symbol and ZapfDingbats. ISO 32000-2 Annex D.1 names Annex D.5 and D.6 as their +/// 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. +/// +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; + + private static readonly string?[] _symbol = BuildEncoding_symbol(); + + private static readonly string?[] _zapfDingbats = BuildEncoding_zapfDingbats(); + + + private static readonly Dictionary _symbolWidths = new() + { + ["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, + }; + + private static readonly Dictionary _zapfDingbatsWidths = new() + { + ["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, + }; + + 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..dbcf1b1a --- /dev/null +++ b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs @@ -0,0 +1,62 @@ +// 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 SymbolFontMetrics' own remarks name as +/// ZapfDingbats.afm-only codes (a85 through a96, a205, a206): +/// they carry ordinary AGL Unicode mappings (the ornamental-bracket block, U+2768–U+2775), and +/// omitting them here would leave the codes that use them (0x80–0x8D) 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) => _map.Value.TryGetValue(name, out unicode!); + + 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)) + 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..39c9d7d0 --- /dev/null +++ b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs @@ -0,0 +1,65 @@ +// 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. + /// + /// + /// Returns silently, with no diagnostic, for /Subtype /Type0 and + /// /Subtype /Type3: PRs 6 and 7 (#98) add readers for those, and reporting + /// here would fire on every CJK or Type 3 + /// document until then, which is not this reader's own limitation to report yet. Not wired to + /// ContentInterpreter in this PR (PR 5 does that), so the only callers today are + /// tests. + /// + internal PdfFontReader? GetFontReader(PdfObject rawFontEntry, DiagnosticSink sink, int? pageIndex) + { + int? objectNumber = null; + int? generation = null; + if (rawFontEntry is PdfIndirectReference r) + { + objectNumber = r.ObjectNumber; + generation = r.Generation; + } + + if (ResolveValue(rawFontEntry) is not PdfDictionary fontDict) + { + sink.Report( + PdfReaderDiagnosticCode.FontUnreadable, "the font resource is not a dictionary.", + objectNumber, generation, pageIndex); + return null; + } + + var subtypeRaw = fontDict.Get(PdfName.Subtype); + var subtype = (subtypeRaw is null ? null : ResolveValue(subtypeRaw)) as PdfName; + 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..8f96be69 100644 --- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs +++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs @@ -562,6 +562,49 @@ 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 building it hit this reader's own indirect-object resolution depth limit + /// ( from PdfDocumentReader.Resolve). 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 a /Differences element was out of range, named a glyph longer than this + /// reader's own name-length bound, or was of a type this reader does not resolve (an indirect + /// reference, legal per §7.3.10, is reported under this code as a reader limitation, not a + /// malformation). Reported once per font. + /// + FontEncodingMalformed = 401, + + /// + /// A simple font's /FirstChar, /LastChar, or /Widths (ISO 32000-2 Table + /// 109) was missing, mistyped, 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). 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. + /// + UnmappedGlyphs = 404, + // ── 9xx: reserved ─────────────────────────────────────────────────────────────────────────── /// @@ -631,6 +674,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..11ccdfd3 --- /dev/null +++ b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs @@ -0,0 +1,53 @@ +// 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()); + } +} diff --git a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs new file mode 100644 index 00000000..988a986b --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs @@ -0,0 +1,129 @@ +// 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 + [InlineData("u110000")] // past U+10FFFF + [InlineData("uni00e9")] // lowercase hex + [InlineData("a__b")] + [InlineData("_a")] + [InlineData("a_")] + [InlineData(".notdef")] + [InlineData("uni0000")] + [InlineData("f_g_nonexistent")] + public void TryMapToUnicode_rejectsTheseNames(string name) + { + Assert.False(AdobeGlyphList.TryMapToUnicode(name, out _)); + } + + [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 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..cc13a529 --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs @@ -0,0 +1,142 @@ +// 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 test over mutated font dictionaries: random /Encoding shapes, +/// /Differences arrays mixing every element type, random /Widths lengths and +/// element types, random /Flags, and random base font names including a 1 KiB one. +/// is asserted to never throw and to report at most four +/// distinct diagnostic codes per font (400 to 402, plus one of 403/404), and +/// over every byte value is asserted to never throw. +/// +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; + } + } + } + + // 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)); + + 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)), + DifferencesGen.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")), + DifferencesGen, + (baseName, diffs) => (PdfObject?)new PdfDictionary() + .Set(new PdfName("BaseEncoding"), new PdfName(baseName)) + .Set(new PdfName("Differences"), diffs))); + + 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))); + + 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))); + + 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]); + + private static readonly Gen FontDictGen = Gen.Select( + BaseFontGen, EncodingGen, WidthsGen, FlagsGen, + (baseFont, encoding, widths, flags) => + { + var dict = new PdfDictionary() + .Set(PdfName.Subtype, "Type1") + .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); + return dict; + }); + + [Fact] + public void Create_neverThrows_reportsAtMostFourDistinctCodes_decodeNeverThrows() + { + using var doc = FontTestSupport.OpenMinimal(); + FontDictGen.Sample( + fontDict => + { + var sink = new DiagnosticSink(cap: 50); + var reader = SimpleFontReader.Create(doc, fontDict, null, null, sink, null); + + 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}"); + } + + for (var b = 0; b < 256; b++) + { + ReadOnlySpan bytes = [(byte)b]; + var offset = 0; + reader.TryDecodeNext(bytes, ref offset, out _); + } + }, + iter: FuzzBudget.Iterations); + } +} diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs new file mode 100644 index 00000000..9afe7a39 --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs @@ -0,0 +1,78 @@ +// 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 real +/// 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])); + } + + 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..7f66cab8 --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs @@ -0,0 +1,233 @@ +// 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 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..6efa3723 --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs @@ -0,0 +1,630 @@ +// 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_reports401WithDoesNotResolveMessage() + { + 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.Contains("does not resolve", 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); + } + + // ── 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); + } + + // ── 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. + } + + // ── 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); + } + + // ── 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); + } + + // ── 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 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 ─────────────────────────────────────────────────────────────────── + + [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); + } + + // ── 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); + } + + // ── 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 31,752 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."); + } + + // ── 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..3ac4a635 --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs @@ -0,0 +1,90 @@ +// Copyright © Timothy van der Ham (@Tim81) +// SPDX-License-Identifier: Apache-2.0 + +using VellumPdf.Fonts; +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 _)); + } + + [Fact] + public void TryGetKernelFont_timesRoman_givesKernelEnum() + { + Assert.True(Standard14Names.TryGetKernelFont("Times-Roman", out var font)); + Assert.Equal(Standard14.TimesRoman, font); + } + + [Fact] + public void TryGetKernelFont_symbol_false() + { + Assert.False(Standard14Names.TryGetKernelFont("Symbol", 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..731b1c05 --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs @@ -0,0 +1,137 @@ +// 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 Symbol.afm/ZapfDingbats.afm directly, via +/// grep N <name> ;, not from the generated file or the Kernel table), and the +/// standard-14 width route () through a live +/// . +/// +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); + } + + // ── Kernel width route, through a real 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_measuresAsQuestionMark_556() + { + var differences = new PdfArray().Add(new PdfInteger(65)).Add(new PdfName("fi")); + var reader = Build(FontDict("Helvetica", differences)); + Assert.Equal(556, 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..16da26ef --- /dev/null +++ b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs @@ -0,0 +1,51 @@ +// 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 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 SymbolFontMetrics' + // own remarks describe as ZapfDingbats.afm-only codes (a85 through a96, a205, a206), which + // Annex D.6 does not document but which do have ordinary AGL Unicode mappings. 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), }; From 01ed1a70dd6c9ba9e818355d4f185cc66a7ec3bd Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 04:32:38 +0200 Subject: [PATCH 2/7] Apply /Encoding to Symbol fonts and the 9.6.5.4 StandardEncoding fill Three corrections to the simple-font reader before review, each found by checking a shipped sentence against ISO 32000-2 or the reader's own API. Symbol and ZapfDingbats took their built-in encoding unconditionally, so a /Differences array or a named /Encoding on them was ignored. 9.6.5.2 says an /Encoding entry "shall override a Type 1 font's mapping", so the two built-in tables are now the Table 112 default base table and /Encoding applies to them as to any other font. HasToUnicode was always false on a parsed file: it tested Resolve(...) is PdfStream, and PdfDocumentReader.Resolve returns a stream object's dictionary, never a stream. It now follows the reference with ResolveStream (7.3.8.1 makes a stream indirect) and keeps a direct PdfStream arm for dictionaries built in memory. A true value also withholds UnmappedGlyphs, since the unparsed stream may map the code. The class remarks claimed 9.6.5.4's closing step, "any undefined entries in the table shall be filled using StandardEncoding", could change no cell. It changes twelve: the StandardEncoding codes MacRomanEncoding leaves undefined (0xAD, 0xB2, 0xB3, 0xB6 to 0xBA, 0xBD, 0xC3, 0xC5, 0xC6). The fill is applied with the clause's own scope: TrueType, Nonsymbolic (read from the Symbolic bit, which Table 121 makes exclusive with it), a dictionary /Encoding, after /Differences, and not over the all-null MacExpert table. A KAT pins the twelve-code set and WinAnsi's empty one; reader tests cover the fill and its four non-cases. Also: encoding-table remarks corrected for the three Annex D.2 footnotes, Standard14Names doc, brief-section references removed from shipped comments, NOTICE trademark line for ITC Zapf Dingbats. --- NOTICE | 2 + src/VellumPdf.Reader/Fonts/FontCache.cs | 2 +- src/VellumPdf.Reader/Fonts/PdfFontReader.cs | 15 +- .../Fonts/SimpleFontEncodings.cs | 16 +- .../Fonts/SimpleFontReader.cs | 118 +++++++++----- src/VellumPdf.Reader/Fonts/Standard14Names.cs | 11 +- .../PdfDocumentReader.Fonts.cs | 7 +- src/VellumPdf.Reader/PdfReaderDiagnostic.cs | 4 +- .../Fonts/FontTestSupport.cs | 2 +- .../Fonts/SimpleFontEncodingsTests.cs | 44 ++++++ .../Fonts/SimpleFontReaderTests.cs | 146 ++++++++++++++++++ .../Fonts/SymbolFontMetricsTests.cs | 2 +- 12 files changed, 304 insertions(+), 65 deletions(-) diff --git a/NOTICE b/NOTICE index 1c31229a..9af8818e 100644 --- a/NOTICE +++ b/NOTICE @@ -88,6 +88,8 @@ Adobe Core 14 AFM font metrics (Symbol, ZapfDingbats) All rights reserved. Comment Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved. + 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 diff --git a/src/VellumPdf.Reader/Fonts/FontCache.cs b/src/VellumPdf.Reader/Fonts/FontCache.cs index ffb9c3e6..818c09ab 100644 --- a/src/VellumPdf.Reader/Fonts/FontCache.cs +++ b/src/VellumPdf.Reader/Fonts/FontCache.cs @@ -14,7 +14,7 @@ namespace VellumPdf.Reader.Fonts; /// 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 (itself far past what any real PDF carries) is worth building for, +/// 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. /// internal sealed class FontCache diff --git a/src/VellumPdf.Reader/Fonts/PdfFontReader.cs b/src/VellumPdf.Reader/Fonts/PdfFontReader.cs index e3047cd5..4d84dfe5 100644 --- a/src/VellumPdf.Reader/Fonts/PdfFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/PdfFontReader.cs @@ -14,9 +14,10 @@ namespace VellumPdf.Reader.Fonts; /// 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). This PR's populates this only from the glyph-name -/// route (the AGL, or the ZapfDingbats list); the higher-priority /ToUnicode route is parsed -/// starting in a later PR, tracked by until then. +/// 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( @@ -37,9 +38,11 @@ internal abstract class PdfFontReader public abstract bool TryDecodeNext(ReadOnlySpan bytes, ref int offset, out DecodedGlyph glyph); /// - /// Whether this font's dictionary names a /ToUnicode stream (§9.10.3). This PR only - /// records the fact; a later PR parses the stream and gives it priority over the glyph-name - /// route in , per §9.10.2's own ordering. + /// 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 index 050a8e2a..b423b5c6 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs @@ -9,8 +9,11 @@ namespace VellumPdf.Reader.Fonts; /// 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 (Annex D.1: "PDF processors shall not have a predefined -/// encoding named StandardEncoding" governs the name lookup, not the table's own correctness). +/// 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) @@ -28,8 +31,8 @@ namespace VellumPdf.Reader.Fonts; /// /Differences entry, per the footnotes' own example. /// /// src/VellumPdf.Conformance/Rules/Fonts/SimpleFontEncoding.cs carries its own copy of -/// these three tables, deliberately not touched by this reader (no file under -/// VellumPdf.Conformance changes in this PR). It diverges from the tables here at exactly +/// 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) @@ -68,7 +71,10 @@ internal static class SimpleFontEncodings /// MacExpertEncoding) 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. + /// 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; diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs index e2c71c10..f01e55cc 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -19,8 +19,19 @@ namespace VellumPdf.Reader.Fonts; /// 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 a real font program would -/// otherwise supply an answer this reader cannot. +/// 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 when the font is nonsymbolic (Table 121 +/// makes the Symbolic and Nonsymbolic flags exclusive, so the clause's Nonsymbolic condition is +/// read from the Symbolic bit). The only cells it 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 /// before its type is tested (one hop, a dangling @@ -35,9 +46,6 @@ internal sealed class SimpleFontReader : PdfFontReader { private static readonly PdfName _fontDescriptorKey = new("FontDescriptor"); private static readonly PdfName _flagsKey = new("Flags"); - private static readonly PdfName _fontFileKey = new("FontFile"); - private static readonly PdfName _fontFile2Key = new("FontFile2"); - private static readonly PdfName _fontFile3Key = new("FontFile3"); private static readonly PdfName _baseEncodingKey = new("BaseEncoding"); private static readonly PdfName _differencesKey = new("Differences"); private static readonly PdfName _firstCharKey = new("FirstChar"); @@ -127,7 +135,7 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) $"has no usable /BaseFont: {excerpt}."); } - // Step 3: symbolic / embedded. + // Step 3: symbolic. var descriptor = Resolve(reader, fontDict.Get(_fontDescriptorKey)) as PdfDictionary; bool symbolic; if (descriptor is not null && Resolve(reader, descriptor.Get(_flagsKey)) is PdfInteger flags) @@ -139,27 +147,21 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) symbolic = afmName is "Symbol" or "ZapfDingbats"; } - var embedded = descriptor is not null - && (Resolve(reader, descriptor.Get(_fontFileKey)) is PdfStream - || Resolve(reader, descriptor.Get(_fontFile2Key)) is PdfStream - || Resolve(reader, descriptor.Get(_fontFile3Key)) is PdfStream); - _ = embedded; // Table 112's embedded/not-embedded split collapses to one rule here; see TableDefault. - - // Step 4: base table. - string?[] table; - if (afmName == "Symbol") - { - table = SymbolFontMetrics.SymbolEncoding.ToArray(); - } - else if (afmName == "ZapfDingbats") + // 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) { - table = SymbolFontMetrics.ZapfDingbatsEncoding.ToArray(); - } - else - { - table = ResolveEncodingTable(reader, fontDict, symbolic, out var encodingDict); - if (encodingDict is not null) - ApplyDifferences(reader, encodingDict, table); + ApplyDifferences(reader, encodingDict, table); + + // Step 5: §9.6.5.4's closing rule (see the class remarks for its exact scope). + var trueType = Resolve(reader, fontDict.Get(PdfName.Subtype)) is PdfName { Value: "TrueType" }; + if (trueType && !symbolic && standardFillAllowed) + FillUndefinedFromStandard(table); } _names = table; @@ -198,8 +200,17 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) if (usesAfmWidths) FillAfmWidths(afmName!, table, unicode, widths, descriptorMissingWidth); - // /ToUnicode: recorded only, not parsed; a later PR adds that (see PdfFontReader's doc). - _hasToUnicode = Resolve(reader, fontDict.Get(_toUnicodeKey)) is PdfStream; + // /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. @@ -211,15 +222,21 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) } } + // 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, out PdfDictionary? encodingDict) + 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); + return TableDefault(symbolic, afmName); case PdfName named: if (SimpleFontEncodings.TryGetNamed(named.Value, out var byName)) @@ -227,33 +244,48 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, $"/Encoding names an encoding this reader does not know: " + $"{DiagnosticExcerpt.Quote(named.Value)}."); - return TableDefault(symbolic); + 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); + return TableDefault(symbolic, afmName); if (baseEncoding is PdfName baseName && SimpleFontEncodings.TryGetNamed(baseName.Value, out var baseTable)) + { + standardFillAllowed = baseName.Value != "MacExpertEncoding"; return baseTable.ToArray(); + } ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, "/Encoding's /BaseEncoding names an encoding this reader does not know."); - return TableDefault(symbolic); + return TableDefault(symbolic, afmName); default: ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, "/Encoding is neither a known encoding name nor an encoding dictionary."); - return TableDefault(symbolic); + return TableDefault(symbolic, afmName); } } - // Table 112's default base encoding: the standard reads embedded → the font program's own - // built-in encoding, not embedded → StandardEncoding (nonsymbolic) or the built-in encoding - // (symbolic). This reader never parses a font program, so both branches land on the same - // answer regardless of embedding (StandardEncoding for nonsymbolic, all-null for symbolic), - // which is why "embedded" plays no part in this method itself (see the class doc's own note). - private static string?[] TableDefault(bool symbolic) => - symbolic ? new string?[256] : SimpleFontEncodings.Standard.ToArray(); + // 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) { @@ -404,8 +436,10 @@ public override bool TryDecodeNext(ReadOnlySpan bytes, ref int offset, out 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) + 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."); diff --git a/src/VellumPdf.Reader/Fonts/Standard14Names.cs b/src/VellumPdf.Reader/Fonts/Standard14Names.cs index a6f59184..767cb519 100644 --- a/src/VellumPdf.Reader/Fonts/Standard14Names.cs +++ b/src/VellumPdf.Reader/Fonts/Standard14Names.cs @@ -8,16 +8,19 @@ 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 -/// built-in encoding and AFM-width fallback §9.6.2.1 requires when a font has no -/// /Widths//FontDescriptor. +/// 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 (§3.9 step 9), never a glyph mapping, which continues to come -/// from the font's own /Encoding resolution regardless of which alias matched. +/// 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 { diff --git a/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs index 39c9d7d0..7ac13cb5 100644 --- a/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs +++ b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs @@ -18,11 +18,10 @@ public sealed partial class PdfDocumentReader /// /// /// Returns silently, with no diagnostic, for /Subtype /Type0 and - /// /Subtype /Type3: PRs 6 and 7 (#98) add readers for those, and reporting + /// /Subtype /Type3: readers for those are not built yet (#98), and reporting /// here would fire on every CJK or Type 3 - /// document until then, which is not this reader's own limitation to report yet. Not wired to - /// ContentInterpreter in this PR (PR 5 does that), so the only callers today are - /// tests. + /// document until they are. Not yet wired to ContentInterpreter, so the only callers + /// today are tests. /// internal PdfFontReader? GetFontReader(PdfObject rawFontEntry, DiagnosticSink sink, int? pageIndex) { diff --git a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs index 8f96be69..6c50672d 100644 --- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs +++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs @@ -601,7 +601,9 @@ public enum PdfReaderDiagnosticCode /// /// 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. + /// 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, diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs index 9afe7a39..b304fa98 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs @@ -9,7 +9,7 @@ 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 real +/// a live /// to resolve indirect references through, with exact control over /// object shapes a document writer would never produce. /// diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs index 7f66cab8..e811fe37 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontEncodingsTests.cs @@ -215,6 +215,50 @@ public void MacExpert_isAllNull() 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() { diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs index 6efa3723..37144e7e 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs @@ -277,6 +277,93 @@ public void EmbeddedNonsymbolicTrueType_noEncoding_usesStandardEncoding() 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); + } + + [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); + } + // ── 9: Symbol / ZapfDingbats base fonts ────────────────────────────────────────────────────── [Fact] @@ -315,6 +402,46 @@ public void ZapfDingbatsBaseFont() 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] @@ -353,6 +480,25 @@ public void WinAnsiFont_unmappedDifferenceName_reports404OnceOnFirstDecode_never 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] diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs index 731b1c05..19c683e5 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs @@ -36,7 +36,7 @@ public void ZapfDingbatsWidths_pinnedEntries() Assert.Equal(202, SymbolFontMetrics.ZapfDingbatsWidths.Count); } - // ── Kernel width route, through a real SimpleFontReader ───────────────────────────────────── + // ── Kernel width route, through SimpleFontReader ──────────────────────────────────────────── private static PdfDictionary FontDict(string baseFont, PdfArray? differences = null) { From 5b76de7235ee9c56fde1a8c36740f0d1c5a06d49 Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 07:56:45 +0200 Subject: [PATCH 3/7] fix(reader): guard font resolution depth, fix AFM width fallback Exception safety: GetFontReader's own two ResolveValue calls were outside any try/catch, so a font entry naming a stream whose /Length chains deep enough to hit MaxResolveDepth threw past the caller, instead of reporting 400 the way SimpleFontReader.Create already does for the same condition. Both calls now share one try/catch, reporting FontUnreadable and returning null. A dedicated regression test builds a 120-deep /Length chain at the font entry itself. AFM width fallback, the substantive behavioural fix: the old route measured a standard-14 font's glyph by mapping its name to Unicode and asking the Kernel's Standard14Metrics.GetWidth for that character, which substitutes the '?' glyph's advance for anything outside WinAnsiEncoding. Every AFM name a text font's own encoding can reach that WinAnsi does not cover (fi, lslash, fraction, and more) measured as the width of a question mark instead of its own. eng/generate-symbol-font-metrics.py now reads all fourteen Core-14 AFM files (Adobe Core-14 AFM files, MustRead.html, Adobe Systems, 1997) and emits a glyph-name-keyed width table per text font, so the lookup needs no Unicode round trip and no WinAnsi dependency. The Kernel is untouched. Every text font gets known-answer tests on a name outside WinAnsi plus one inside, read from the AFM files directly. /Differences: a present-but-wrong-typed entry now reports 401 instead of being silently dropped as absent (it previously matched the same code path as an omitted or null entry). An element this reader cannot resolve now stops the array being applied further, instead of resuming with the running code unchanged, which let a later name silently overwrite an earlier code's own assignment. TrueType StandardEncoding fill: runs only with a present /FontDescriptor and /Flags, since 9.6.5.4 conditions it on "the font descriptor's Nonsymbolic flag", and Table 109 makes the descriptor required outside PDF 1.0 to 1.7's standard 14. A missing descriptor no longer authorises the fill by default. With a descriptor present the state is read from the Symbolic flag, as step 3 already reads it, per 9.8.2 ("A PDF processor should always check the Symbolic flag"); a descriptor whose two flags disagree is read the same way at both steps. A two-case Theory pins both disagreeing shapes. /BaseEncoding chain message: a reference still unresolved after one hop now says so, rather than "names an encoding this reader does not know", which is only true of a bad name. AGL allocation: TryMapToUnicode returns the mapped string directly for a single-component name instead of routing it through a StringBuilder. Re-measured with GC.GetTotalMemory(true) before and after building 10,000 SimpleFontReader instances, kept reachable: about 6,464 B each (about 62 MiB at the FontCache cap), down from about 9,872 B; through GetFontReader's full cache, about 7,639 B each (about 73 MiB at the cap), down from about 10,495 B. Both figures are recorded in FontCache.MaxCachedFonts' own doc. Also: SymbolFontMetrics' width dictionaries are FrozenDictionary, not a mutable Dictionary behind IReadOnlyDictionary; the generated header lists all fourteen AFM inputs with their own Version line and normalised SHA-256; ZapfDingbatsGlyphList.TryMap and the AGL class remarks no longer misstate a departure that is not one; FontFuzzTests drives GetFontReader itself as well as SimpleFontReader.Create, with random /Subtype, /ToUnicode and indirect /Encoding shapes; a byte-identity test compares the two AdobeGlyphList.txt copies directly instead of trusting the class doc's own claim; a symbolic TrueType font with a dictionary /Encoding is pinned as a stated departure from the TrueType clause, not a defect; FontUnreadable now cites Table 109's own required entries; Annex D wording fixes for MacExpertEncoding's own title and the 0xCA/0xDB footnote attribution; "Hebrew presentation forms" corrected to "Hebrew letter-plus-point combinations"; five more AGL edge cases pinned; two unguarded char.ConvertFromUtf32 call sites noted as trusting the pinned resource; the 401 message formats a real-valued /Differences with the invariant culture. Declined: a malformed /Widths on a standard-14 font is not repaired from its AFM; one sentence added to the 402 doc instead of changing behaviour, out of scope here. Resolve-scope sweep (every ResolveValue/Resolve/ResolveStream call in Reader/Fonts/ and PdfDocumentReader.Fonts.cs): - SimpleFontReader.cs: all twelve Resolve(reader, ...) call sites, the ResolveStream call for /ToUnicode, and the Resolve(reader, raw) helper itself are inside Create's own try/catch (already protected, unchanged here). - PdfDocumentReader.Fonts.cs: the two ResolveValue calls (the font entry, then its /Subtype) are now inside GetFontReader's own try/catch (this commit's fix; previously unguarded). Wrong-type sweep (every present-but-wrong-typed entry in Reader/Fonts/): - /Differences not an array: reports 401 (this commit's fix; was silently dropped before). - /FirstChar or /LastChar not an integer: reports 402 (unchanged). - /Widths not an array: reports 402 (unchanged). - The font entry not a dictionary: reports 400 (unchanged). - /FontDescriptor not a dictionary (an "as" cast, not "is not"): silently treated as absent. No diagnostic code names a malformed descriptor, and this commit adds no public symbol, so the silence is kept and documented here rather than inventing a code for it. - /MissingWidth not a number (switch default 0.0): same reasoning; falls back to Table 109's own default of 0. - /Subtype not the literal name "TrueType" inside Populate: no diagnostic; GetFontReader itself already validates /Subtype before Create is reached, direct-call test paths aside. Resource growth: SymbolFontMetrics.cs (generated) grew from 24,067 to 131,557 bytes (+107,490 B); VellumPdf.Reader.dll (Debug) grew from 340,992 to 409,088 bytes (+68,096 B), measured by building the same commit before and after in isolated worktrees. Reader.Tests: 1553 passed / 0 failed / 12 skipped, with QPDF_HOME, POPPLER_HOME, VERAPDF_HOME, REQUIRE_ORACLES=1, REQUIRE_VERAPDF=1 set. Conformance.Tests: 1285 passed / 0 failed / 0 skipped, same environment. FontFuzzTests at VELLUMPDF_FUZZ_ITER=60000: both properties pass in 1.568s combined. --- eng/generate-symbol-font-metrics.py | 177 +- src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs | 36 +- src/VellumPdf.Reader/Fonts/FontCache.cs | 10 + .../Fonts/SimpleFontEncodings.cs | 13 +- .../Fonts/SimpleFontReader.cs | 160 +- .../Fonts/SymbolFontMetrics.cs | 3948 ++++++++++++++++- .../Fonts/ZapfDingbatsGlyphList.cs | 15 +- .../PdfDocumentReader.Fonts.cs | 34 +- src/VellumPdf.Reader/PdfReaderDiagnostic.cs | 19 +- .../Fonts/ReaderEncodingParityTests.cs | 19 + .../Fonts/AdobeGlyphListTests.cs | 13 +- .../Fonts/FontFuzzTests.cs | 159 +- .../Fonts/FontTestSupport.cs | 44 + .../Fonts/SimpleFontReaderTests.cs | 162 + .../Fonts/SymbolFontMetricsTests.cs | 72 +- .../Fonts/ZapfDingbatsGlyphListTests.cs | 9 + 16 files changed, 4742 insertions(+), 148 deletions(-) diff --git a/eng/generate-symbol-font-metrics.py b/eng/generate-symbol-font-metrics.py index 75421a36..ee66897e 100644 --- a/eng/generate-symbol-font-metrics.py +++ b/eng/generate-symbol-font-metrics.py @@ -1,25 +1,30 @@ # Copyright © Timothy van der Ham (@Tim81) # SPDX-License-Identifier: Apache-2.0 # -# Generates src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs from the Adobe Core 14 AFM files -# (MustRead.html, Adobe Systems, 1997) for Symbol.afm and ZapfDingbats.afm. Those two are the only -# two of the fourteen that are symbolic 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. +# 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 the source files with a normalised SHA-256 manifest, so a +# 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 Symbol.afm and ZapfDingbats.afm (Adobe Core 14 AFM files, MustRead.html, Adobe -# Systems, 1997). defaults to src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs. +# 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 @@ -28,17 +33,60 @@ 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 @@ -54,6 +102,7 @@ ) C_RECORD = re.compile(r"^C (-?\d+) ; WX (-?\d+) ; N (\S+) ;") +VERSION_LINE = re.compile(r"^Version (\S+)$") def normalize(raw_bytes): @@ -83,11 +132,16 @@ def load_afm(afm_dir, filename): def parse_afm(filename, normalized): copyright_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 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) @@ -107,6 +161,9 @@ def parse_afm(filename, normalized): if copyright_line is None: print(f"{filename}: no Comment Copyright 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: @@ -115,7 +172,7 @@ def parse_afm(filename, normalized): ) sys.exit(1) - return records, copyright_line + return records, copyright_line, version def format_encoding(field_name, records): @@ -131,12 +188,13 @@ def format_encoding(field_name, records): def format_widths(field_name, records): body = [ - f" private static readonly Dictionary _{field_name} = new()", + f" private static readonly FrozenDictionary _{field_name}Widths = " + "new Dictionary", " {", ] for _, width, name in records: body.append(f' ["{name}"] = {width},') - body.append(" };") + body.append(" }.ToFrozenDictionary();") return body @@ -156,19 +214,29 @@ def wrap_comment(text, width=96): return lines -def generate_source(symbol_records, symbol_copyright, zapf_records, zapf_copyright): +def generate_source(parsed): + # parsed: filename -> (records, copyright_line, version), 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("//") - for line in wrap_comment(f"Symbol.afm: {symbol_copyright}"): - w(line) - for line in wrap_comment(f"ZapfDingbats.afm: {zapf_copyright}"): - w(line) + 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, _ = parsed[filename] + for line in wrap_comment(f"{filename}: {copyright_line}"): + w(line) w("//") for line in wrap_comment(MUSTREAD_PARAGRAPH): w(line) @@ -179,18 +247,24 @@ def generate_source(symbol_records, symbol_copyright, zapf_records, zapf_copyrig w("namespace VellumPdf.Reader.Fonts;") w("") w("/// ") - w("/// The built-in encodings and AFM advance widths of the two symbolic standard 14 fonts,") - w("/// Symbol and ZapfDingbats. ISO 32000-2 Annex D.1 names Annex D.5 and D.6 as their") - w("/// built-in encodings; the Adobe Core 14 AFM files are this reader's delivery vehicle for") - w("/// that same data, not a separate transcription of the Annex D tables. The Symbol coding") - w("/// here agrees with Annex D.5 at all 189 coded glyphs. The ZapfDingbats coding carries 14") - w("/// codes (0x80 to 0x8D) that Annex D.6 does not document at all; this reader keeps them,") - w("/// on the view that a font program carrying those codes draws them regardless of whether") - w("/// the standard's own table lists them.") + 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) @@ -210,17 +284,56 @@ def generate_source(symbol_records, symbol_copyright, zapf_records, zapf_copyrig 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) - w("") - for line in format_widths("symbolWidths", symbol_records): + + 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("zapfDingbatsWidths", zapf_records): + 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("") @@ -254,12 +367,12 @@ def main(): print("usage: generate-symbol-font-metrics.py --afm-dir [--out | --check]", file=sys.stderr) return 1 - symbol_normalized = load_afm(afm_dir, "Symbol.afm") - zapf_normalized = load_afm(afm_dir, "ZapfDingbats.afm") - symbol_records, symbol_copyright = parse_afm("Symbol.afm", symbol_normalized) - zapf_records, zapf_copyright = parse_afm("ZapfDingbats.afm", zapf_normalized) + parsed = {} + for filename, _, _, _ in FONT_TABLE: + normalized = load_afm(afm_dir, filename) + parsed[filename] = parse_afm(filename, normalized) - text = generate_source(symbol_records, symbol_copyright, zapf_records, zapf_copyright) + text = generate_source(parsed) if check: if not os.path.exists(output): diff --git a/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs index 7a197104..4d9606ce 100644 --- a/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs +++ b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs @@ -10,19 +10,15 @@ namespace VellumPdf.Reader.Fonts; /// 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 presentation -/// forms whose AGL name decomposes into a base letter plus a combining point), so the map's value -/// is a string, not a single . +/// 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 three ways, each because the two ends of the departure are indistinguishable to a caller -/// that only gets a Unicode string back: +/// in two ways, each because the two ends of the departure are indistinguishable to a caller that +/// only gets a Unicode string back: /// -/// Only uppercase uni/u hex digits are recognised. The AGL -/// Specification itself writes the synthetic forms in uppercase; the Conformance package's own -/// copy (src/VellumPdf.Conformance/Rules/Fonts/AdobeGlyphList.cs) additionally accepts -/// lowercase, which this reader does not. /// 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 @@ -31,6 +27,11 @@ namespace VellumPdf.Reader.Fonts; /// .notdef, which the bundled list maps to U+0000, and the literal name /// uni0000. /// +/// 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 { @@ -72,6 +73,21 @@ public static bool TryMapToUnicode(string glyphName, out string unicode) 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. This is where most of FontCache's per-font retained bytes came from + // before this fast path existed (see FontCache.MaxCachedFonts). + if (trimmed.IndexOf('_') < 0) + { + if (!TryMapComponent(map, trimmed, out var single)) + return false; + if (single.Length == 1 && single[0] == '\0') + return false; // .notdef and uni0000 both resolve here; treated as unmapped. + unicode = single; + return true; + } + var result = new System.Text.StringBuilder(); var start = 0; while (start <= trimmed.Length) @@ -210,6 +226,8 @@ private static Dictionary Load() ok = false; break; } + // Unguarded: AdobeGlyphList.txt is a pinned, byte-identity-checked embedded + // resource, never a surrogate half or a value past 0x10FFFF (NOTICE, Count tests). sb.Append(char.ConvertFromUtf32(cp)); } if (ok) diff --git a/src/VellumPdf.Reader/Fonts/FontCache.cs b/src/VellumPdf.Reader/Fonts/FontCache.cs index 818c09ab..a0a55e38 100644 --- a/src/VellumPdf.Reader/Fonts/FontCache.cs +++ b/src/VellumPdf.Reader/Fonts/FontCache.cs @@ -16,6 +16,16 @@ namespace VellumPdf.Reader.Fonts; /// 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). Both figures dropped +/// from an earlier measurement (about 9,872 B and 10,495 B respectively) once +/// AdobeGlyphList.TryMapToUnicode stopped routing a single-component glyph name through a +/// StringBuilder, which was most of the per-font cost. +/// /// internal sealed class FontCache { diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs b/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs index b423b5c6..f1a88729 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontEncodings.cs @@ -42,11 +42,12 @@ namespace VellumPdf.Reader.Fonts; /// 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 Annex D.2's own MacRoman column: 0xCA -/// (Annex D.2 pairs it with space, footnote 6's dual mapping) and 0xDB (Annex D.2 and -/// footnote 1 both read currency; Apple's own 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"). +/// 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 @@ -68,7 +69,7 @@ internal static class SimpleFontEncodings /// /// MacExpertEncoding: every cell is . Annex D.4 (Expert set and - /// MacExpertEncoding) is not transcribed here: no oracle in this test suite exercises it, 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 diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs index f01e55cc..5932a26d 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -1,8 +1,8 @@ // Copyright © Timothy van der Ham (@Tim81) // SPDX-License-Identifier: Apache-2.0 +using System.Globalization; using VellumPdf.Core; -using VellumPdf.Fonts; namespace VellumPdf.Reader.Fonts; @@ -10,7 +10,7 @@ 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, the Kernel's own AFM metrics. +/// 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): @@ -23,15 +23,27 @@ namespace VellumPdf.Reader.Fonts; /// 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 when the font is nonsymbolic (Table 121 -/// makes the Symbolic and Nonsymbolic flags exclusive, so the clause's Nonsymbolic condition is -/// read from the Symbolic bit). The only cells it 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. +/// 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 /// before its type is tested (one hop, a dangling @@ -41,6 +53,30 @@ namespace VellumPdf.Reader.Fonts; /// 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 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. +/// +/// +/// §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 { @@ -100,8 +136,13 @@ internal static SimpleFontReader Create( } catch (InvalidDataException) { - // reader.Resolve throws past MaxResolveDepth (PdfDocumentReader.cs); nothing else in - // Populate is expected to throw, and the fuzz test is the proof that holds. + // 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]; @@ -137,16 +178,30 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) // Step 3: symbolic. 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 (descriptor is not null && Resolve(reader, descriptor.Get(_flagsKey)) is PdfInteger flags) + if (resolvedFlags is not null) { - symbolic = (flags.Value & SymbolicFlagBit) != 0; + 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 @@ -158,9 +213,10 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) { ApplyDifferences(reader, encodingDict, table); - // Step 5: §9.6.5.4's closing rule (see the class remarks for its exact scope). + // 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 && !symbolic && standardFillAllowed) + if (trueType && descriptorNonsymbolic && standardFillAllowed) FillUndefinedFromStandard(table); } @@ -198,7 +254,7 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) // 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, unicode, widths, descriptorMissingWidth); + 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 @@ -257,6 +313,17 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) 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); @@ -289,8 +356,16 @@ private static void FillUndefinedFromStandard(string?[] table) private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDict, string?[] table) { - if (Resolve(reader, encodingDict.Get(_differencesKey)) is not PdfArray differences) + var resolved = Resolve(reader, encodingDict.Get(_differencesKey)); + if (resolved is null or PdfNull) + return; // absent (ISO 32000-2 §7.3.9): omitted, a direct 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++) @@ -337,11 +412,32 @@ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDi default: ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, "/Differences contains an element this reader does not resolve."); - break; // continue with the next element; code is unchanged. + // 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: 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 number {r.Value.ToString(CultureInfo.InvariantCulture)}", + PdfBoolean b => $"the boolean {(b.Value ? "true" : "false")}", + PdfLiteralString or PdfHexString => "a string", + 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). @@ -399,22 +495,16 @@ private bool BuildWidths(PdfDocumentReader reader, PdfDictionary fontDict, doubl return false; } - private static void FillAfmWidths( - string afmName, string?[] table, string?[] unicode, double[] widths, double missingWidth) + // 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. + private static void FillAfmWidths(string afmName, string?[] table, double[] widths, double missingWidth) { - if (Standard14Names.TryGetKernelFont(afmName, out var font)) - { - for (var code = 0; code < 256; code++) - { - if (table[code] is null) - continue; - var u = unicode[code]; - widths[code] = u is { Length: 1 } ? Standard14Metrics.GetWidth(font, u[0]) : missingWidth; - } - return; - } - - var byName = afmName == "Symbol" ? SymbolFontMetrics.SymbolWidths : SymbolFontMetrics.ZapfDingbatsWidths; + 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]; diff --git a/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs index 78121666..7b68f072 100644 --- a/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs +++ b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs @@ -1,12 +1,69 @@ // 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. +// Helvetica.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. +// All Rights Reserved. +// Helvetica-Bold.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// Helvetica-Oblique.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// Helvetica-BoldOblique.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// Times-Roman.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// Times-Bold.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// Times-Italic.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// Times-BoldItalic.afm: Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems +// Incorporated. All Rights Reserved. +// 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 @@ -21,14 +78,18 @@ namespace VellumPdf.Reader.Fonts; /// -/// The built-in encodings and AFM advance widths of the two symbolic standard 14 fonts, -/// Symbol and ZapfDingbats. ISO 32000-2 Annex D.1 names Annex D.5 and D.6 as their -/// 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 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 { @@ -48,12 +109,3858 @@ internal static class SymbolFontMetrics /// 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 Dictionary _symbolWidths = new() + 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, @@ -245,9 +4152,9 @@ internal static class SymbolFontMetrics ["bracerightmid"] = 494, ["bracerightbt"] = 494, ["apple"] = 790, - }; + }.ToFrozenDictionary(); - private static readonly Dictionary _zapfDingbatsWidths = new() + private static readonly FrozenDictionary _zapfDingbatsWidths = new Dictionary { ["space"] = 278, ["a1"] = 974, @@ -451,7 +4358,24 @@ internal static class SymbolFontMetrics ["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() { diff --git a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs index dbcf1b1a..6223f5a4 100644 --- a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs +++ b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs @@ -35,7 +35,16 @@ internal static class ZapfDingbatsGlyphList /// _-composition) to its Unicode code point. Returns when the /// name is not in the list. /// - public static bool TryMap(string name, out string unicode) => _map.Value.TryGetValue(name, out unicode!); + 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() { @@ -55,7 +64,11 @@ private static Dictionary Load() 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 index 7ac13cb5..9b1cecd2 100644 --- a/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs +++ b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs @@ -22,6 +22,17 @@ public sealed partial class PdfDocumentReader /// 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) { @@ -33,16 +44,31 @@ public sealed partial class PdfDocumentReader generation = r.Generation; } - if (ResolveValue(rawFontEntry) is not PdfDictionary fontDict) + 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, "the font resource is not a dictionary.", + PdfReaderDiagnosticCode.FontUnreadable, + "building this font hit the reader's own indirect-object resolution depth limit.", objectNumber, generation, pageIndex); return null; } - var subtypeRaw = fontDict.Get(PdfName.Subtype); - var subtype = (subtypeRaw is null ? null : ResolveValue(subtypeRaw)) as PdfName; switch (subtype?.Value) { case "Type1" or "MMType1" or "TrueType": diff --git a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs index 6c50672d..3a449b82 100644 --- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs +++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs @@ -569,18 +569,21 @@ public enum PdfReaderDiagnosticCode /// /BaseFont, named a /Subtype this reader knows nothing about (not /// /Type0 or /Type3, which are silent until this reader gains readers for them), /// or building it hit this reader's own indirect-object resolution depth limit - /// ( from PdfDocumentReader.Resolve). Reported once - /// per font. + /// ( from PdfDocumentReader.Resolve). 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 a /Differences element was out of range, named a glyph longer than this - /// reader's own name-length bound, or was of a type this reader does not resolve (an indirect - /// reference, legal per §7.3.10, is reported under this code as a reader limitation, not a - /// malformation). Reported once per font. + /// 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, or was of a type this reader does not resolve (an + /// indirect reference, legal per §7.3.10, is reported under this code as a reader limitation, + /// not a malformation, and stops the array from being applied any further). Reported once per + /// font. /// FontEncodingMalformed = 401, @@ -588,7 +591,9 @@ public enum PdfReaderDiagnosticCode /// A simple font's /FirstChar, /LastChar, or /Widths (ISO 32000-2 Table /// 109) was missing, mistyped, 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). Reported once per font. + /// 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, diff --git a/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs index 11ccdfd3..bd2b6f76 100644 --- a/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs +++ b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs @@ -50,4 +50,23 @@ public void MacRoman_differsAtExactlyTheSeventeenCodes() ]; 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; nothing before this test compared the two files. + [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 index 988a986b..d154729e 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs @@ -49,7 +49,8 @@ public void TryMapToUnicode_u1F600_givesTheSurrogatePair() [InlineData("uni004")] // short group [InlineData("uni00410")] // 5 digits [InlineData("uni0041x")] - [InlineData("uniD800")] // surrogate + [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")] @@ -58,11 +59,21 @@ public void TryMapToUnicode_u1F600_givesTheSurrogatePair() [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() { diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs index cc13a529..865480b3 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs @@ -8,12 +8,17 @@ namespace VellumPdf.Reader.Tests.Fonts; /// -/// CsCheck property test over mutated font dictionaries: random /Encoding shapes, -/// /Differences arrays mixing every element type, random /Widths lengths and -/// element types, random /Flags, and random base font names including a 1 KiB one. -/// is asserted to never throw and to report at most four -/// distinct diagnostic codes per font (400 to 402, plus one of 403/404), and -/// over every byte value is asserted to never throw. +/// 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, random /Widths lengths and element types, 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 { @@ -31,6 +36,24 @@ internal static long Iterations } } + // 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 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(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. @@ -56,6 +79,11 @@ internal static long Iterations Gen.Const((PdfObject?)new PdfName("MacRomanEncoding")), Gen.Const((PdfObject?)new PdfName("Bogus")), Gen.Const((PdfObject?)new PdfInteger(42)), + // 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)), DifferencesGen.Select(diffs => { var dict = new PdfDictionary().Set(new PdfName("Differences"), diffs); @@ -85,13 +113,32 @@ internal static long Iterations 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, - (baseFont, encoding, widths, flags) => + BaseFontGen, EncodingGen, WidthsGen, FlagsGen, SubtypeGen, ToUnicodeGen, + (baseFont, encoding, widths, flags, subtype, toUnicode) => { - var dict = new PdfDictionary() - .Set(PdfName.Subtype, "Type1") - .Set(PdfName.BaseFont, baseFont); + 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) @@ -102,41 +149,85 @@ internal static long Iterations } 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 = FontTestSupport.OpenMinimal(); + 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); + } - 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}"); - } - - for (var b = 0; b < 256; b++) - { - ReadOnlySpan bytes = [(byte)b]; - var offset = 0; - reader.TryDecodeNext(bytes, ref offset, out _); - } + // 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); + iter: FuzzBudget.Iterations, threads: 1); } } diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs index b304fa98..0d3b5233 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontTestSupport.cs @@ -35,6 +35,50 @@ internal static PdfDocumentReader Open(params Obj[] 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(); diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs index 37144e7e..c7915068 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs @@ -176,6 +176,71 @@ public void Differences_nameLongerThanBound_reports401Once_codeStaysUndefined() 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. Before the fix this reader resumed + // after the bad element with the running code unchanged, so /zcaron landed on B (0x42) and + // /Zcaron on C (0x43) instead of being skipped; both must keep their StandardEncoding + // names here. + 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 resolve", d.Message); + } + // ── 6: /Encoding shapes ────────────────────────────────────────────────────────────────────── [Fact] @@ -217,6 +282,26 @@ public void Encoding_standardEncodingName_acceptedSilently() 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] @@ -349,6 +434,27 @@ public void SymbolicTrueType_dictionaryWithMacRomanBase_isNotFilledFromStandard( 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() { @@ -364,6 +470,44 @@ public void NonsymbolicTrueType_dictionaryWithMacExpertBase_isNotFilledFromStand Assert.Null(Decode(reader, 0xB2).Unicode); } + [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] @@ -695,6 +839,24 @@ public void GetFontReader_twoDirectDictionaries_returnTwoInstances() 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); + } + // ── 14: diagnostics carry object number, generation, page index ───────────────────────────── [Fact] diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs index 19c683e5..36c44ed1 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SymbolFontMetricsTests.cs @@ -10,10 +10,10 @@ namespace VellumPdf.Reader.Tests.Fonts; /// /// Pins ' generated widths against the AFM files (every number -/// here was read from Symbol.afm/ZapfDingbats.afm directly, via -/// grep N <name> ;, not from the generated file or the Kernel table), and the -/// standard-14 width route () through a live -/// . +/// 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 { @@ -36,7 +36,62 @@ public void ZapfDingbatsWidths_pinnedEntries() Assert.Equal(202, SymbolFontMetrics.ZapfDingbatsWidths.Count); } - // ── Kernel width route, through SimpleFontReader ──────────────────────────────────────────── + // ── 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) { @@ -81,11 +136,14 @@ public void Helvetica_codeSpace_width278() } [Fact] - public void FiOutsideWinAnsi_measuresAsQuestionMark_556() + 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(556, WidthOf(reader, 0x41)); + Assert.Equal(500, WidthOf(reader, 0x41)); } [Fact] diff --git a/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs index 16da26ef..56920cf4 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs @@ -36,6 +36,15 @@ 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() { From bc91c67b0dcb7fea072cffff26a182e0f4d78aed Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 13:22:17 +0200 Subject: [PATCH 4/7] fix(reader): treat null /Widths and /Encoding as absent per spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ISO 32000-2 §7.3.7: a dictionary entry whose value is null is treated the same as if the entry does not exist. The shared Resolve helper in SimpleFontReader now normalises a resolved PdfNull, direct or reached through one hop, to null, so /Encoding, /Widths, /FirstChar, /LastChar, /BaseFont, /FontDescriptor, /MissingWidth, /BaseEncoding, /Flags and /Differences all get that rule from one site instead of only ApplyDifferences special-casing it locally. /Differences also stops charging an allocation to every oversized glyph name once the first is reported: the interpolated message and its DiagnosticExcerpt.Quote are now built only when the 401 flag is still clear, since ReportOnce would otherwise discard everything past the first anyway. Its default arm now names what /Differences permits (integers and names only, an indirect reference not followed) instead of calling every other type "unresolved". Also: the AFM-derived font-metrics header now carries all three trademark sentences the Core-14 AFM files' own Notice lines add after their copyright text, not just ZapfDingbats'; NOTICE is retitled and lists every font's own copyright line; dead Standard14Names code with no production caller is removed; several comments overstated what the Adobe Glyph List or ZapfDingbats list map, and four more are reworded as present-tense design statements rather than commentary on prior code; the two per-font arrays SimpleFontReader kept as unused field initialisers are dropped; GetFontReader now checks disposal up front, like every other entry point; and the font fuzz generator gains a non-array /Differences arm and indirect-reference /Widths arms neither generator could draw before. Findings addressed, each fixed unless marked declined: 1 (/Widths, /Encoding null): fixed. Pinning tests: Widths_directNull_treatedAsAbsent_usesAfmWidth_no402, Widths_referenceToNullObject_treatedAsAbsent_usesAfmWidth_no402, Encoding_directNull_treatedAsAbsent_usesStandardEncoding_no401, Encoding_referenceToNullObject_treatedAsAbsent_no401, Widths_wrongTypeNotNull_stillReports402 (negative control). 2 (NOTICE/generator incomplete): fixed. SymbolFontMetrics.cs regenerated from the generator change; diff is header-only, confirmed by generate-symbol-font-metrics.py --check. 3 (AFM fill gated on /Flags): declined. FillAfmWidths has no such gate; the description matches the unrelated TrueType encoding-table fill instead, which is gated for a different reason. Adding the gate to width-filling would contradict Table 109 (/FontDescriptor optional for the standard 14) and break Helvetica_noEncoding_noWidths_nonsymbolic, an existing test with no /FontDescriptor at all. Added Helvetica_noWidths_descriptorPresentNoFlags_stillFillsAfmWidth to pin the fill still firing in that shape. 4 (401 message says "unresolved"): fixed. Pinning tests: Differences_realElement_reports401WithExactMessage, Differences_indirectReferenceElement_reports401WithExactMessage. 5 (wrong subset-tag clause): fixed, §9.9.1 to §9.9.2, checked against the local ISO copy. 6 (.notdef doc overstates the U+0000 rule): fixed, comment only; existing rejection KATs for ".notdef" and "uni0000" already pin the actual behaviour. 7 (ZapfDingbats "ordinary AGL" claim): fixed, comment only; existing ZapfDingbatsGlyphListTests already pin the actual behaviour. 8 (round-1 allocation fix unpinned): fixed. Pinning test: TryMapToUnicode_singleComponentName_returnsTheSameInstanceEachCall. 9 (dead TryGetKernelFont): fixed, removed; grep -rn confirms no remaining callers in src or tests. 10 (FillAfmWidths trade unrecorded): fixed, comment only. 11 (AGL pin comment cites the wrong evidence): fixed, comment only. 12 (AGL edge-case KATs): fixed. Pinning tests: TryMapToUnicode_underscoreAlone_false, TryMapToUnicode_uni0041_B_composesAB, TryMapToUnicode_lengthBoundary_exactly128Characters_accepted, TryMapToUnicode_uni0000_uni0000_composesTwoNulCharacters, TryMapToUnicode_uniSurrogateBoundary. All five agree with the existing class doc. 13 (stale narration in shipped comments): fixed in AdobeGlyphList.cs, FontCache.cs, and ReaderEncodingParityTests.cs, plus one more of the same kind found in SimpleFontReaderTests.cs while editing that test. 14 (/Flags of the wrong type tolerated silently): declined a behaviour change; added a comment explaining why a fifth diagnostic code is not added for this producer defect. 15 (/Differences over-length names allocate before the guard): fixed. Pinning test: Create_allocatesUnder64KiB_forA100000ElementDifferencesArray. Measured before 44,012,360 bytes, after 13,336 bytes. 16 (two fuzz shapes unreachable): fixed, new generator arms for a non-array /Differences and an indirect-reference /Widths (as an element and as the whole value). A coverage change, not a behaviour change, so no single test moves from failing to passing; both properties still pass at VELLUMPDF_FUZZ_ITER=60000 (about 1.4 s). 17 (GetFontReader on a disposed reader): fixed. Pinning test: GetFontReader_disposedReader_throwsObjectDisposedException. 18 (a rejected font is never cached): documented in FontCache's own remarks; no behaviour change. 19 (FontUnreadable doc names an internal member): fixed, doc only. 20 (_names/_widths/_unicode allocated twice): fixed. The existing Create_allocatesUnder64KiB_forA100000ElementWidthsArray KAT's own measured figure moved from 31,752 to 6,464 bytes; its comment is updated, and its 64 KiB bound is unchanged since that bound was already stated as generous, not tight. --- NOTICE | 37 +++- eng/generate-symbol-font-metrics.py | 30 ++- src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs | 26 ++- src/VellumPdf.Reader/Fonts/FontCache.cs | 20 +- .../Fonts/SimpleFontReader.cs | 81 ++++++-- src/VellumPdf.Reader/Fonts/Standard14Names.cs | 28 +-- .../Fonts/SymbolFontMetrics.cs | 11 + .../Fonts/ZapfDingbatsGlyphList.cs | 11 +- .../PdfDocumentReader.Fonts.cs | 6 + src/VellumPdf.Reader/PdfReaderDiagnostic.cs | 14 +- .../Fonts/ReaderEncodingParityTests.cs | 3 +- .../Fonts/AdobeGlyphListTests.cs | 62 ++++++ .../Fonts/FontFuzzTests.cs | 55 ++++- .../Fonts/SimpleFontReaderTests.cs | 193 +++++++++++++++++- .../Fonts/Standard14NamesTests.cs | 14 -- .../Fonts/ZapfDingbatsGlyphListTests.cs | 12 +- 16 files changed, 480 insertions(+), 123 deletions(-) diff --git a/NOTICE b/NOTICE index 9af8818e..e2537f55 100644 --- a/NOTICE +++ b/NOTICE @@ -76,18 +76,43 @@ ZapfDingbats glyph list 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 (Symbol, ZapfDingbats) +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 encoding and advance widths for the Symbol and ZapfDingbats - standard 14 fonts (ISO 32000-2 Annex D.1, D.5, D.6) + 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. @@ -100,9 +125,9 @@ Adobe Core 14 AFM font metrics (Symbol, ZapfDingbats) or obligation to support the use of the AFM files. This entry exists because that licence conditions redistribution on the - copyright notices above being retained; SymbolFontMetrics.cs is a derived - table of glyph names, codes and advance widths, not a copy of the AFM files - themselves. + copyright and trademark notices above being retained; SymbolFontMetrics.cs + is a derived table of glyph names, codes and advance widths, not a copy of + the AFM files themselves. ──────────────────────────────────────────────────────────────────────────────── 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 index ee66897e..ce1f3b00 100644 --- a/eng/generate-symbol-font-metrics.py +++ b/eng/generate-symbol-font-metrics.py @@ -104,6 +104,15 @@ 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") @@ -132,12 +141,15 @@ def load_afm(afm_dir, filename): 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: @@ -161,6 +173,9 @@ def parse_afm(filename, normalized): 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) @@ -172,7 +187,11 @@ def parse_afm(filename, normalized): ) sys.exit(1) - return records, copyright_line, version + 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): @@ -215,7 +234,7 @@ def wrap_comment(text, width=96): def generate_source(parsed): - # parsed: filename -> (records, copyright_line, version), in FONT_TABLE order. + # parsed: filename -> (records, copyright_line, version, trademark), in FONT_TABLE order. o = [] w = o.append @@ -229,14 +248,17 @@ def generate_source(parsed): 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] + _, _, version, _ = parsed[filename] w(f"// {filename} (Version {version})") w(f"// {MANIFEST[filename]}") w("//") for filename, _, _, _ in FONT_TABLE: - _, copyright_line, _ = parsed[filename] + _, 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) diff --git a/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs index 4d9606ce..8ea8c15a 100644 --- a/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs +++ b/src/VellumPdf.Reader/Fonts/AdobeGlyphList.cs @@ -23,9 +23,10 @@ namespace VellumPdf.Reader.Fonts; /// 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 both -/// .notdef, which the bundled list maps to U+0000, and the literal name -/// uni0000. +/// 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 @@ -76,14 +77,17 @@ public static bool TryMapToUnicode(string glyphName, out string unicode) // 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. This is where most of FontCache's per-font retained bytes came from - // before this fast path existed (see FontCache.MaxCachedFonts). + // 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; // .notdef and uni0000 both resolve here; treated as unmapped. + return false; unicode = single; return true; } @@ -107,8 +111,11 @@ public static bool TryMapToUnicode(string glyphName, out string unicode) 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; // .notdef and uni0000 both resolve here; treated as unmapped. + return false; unicode = result.ToString(); return true; @@ -226,8 +233,9 @@ private static Dictionary Load() ok = false; break; } - // Unguarded: AdobeGlyphList.txt is a pinned, byte-identity-checked embedded - // resource, never a surrogate half or a value past 0x10FFFF (NOTICE, Count tests). + // 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) diff --git a/src/VellumPdf.Reader/Fonts/FontCache.cs b/src/VellumPdf.Reader/Fonts/FontCache.cs index a0a55e38..a28a9bb2 100644 --- a/src/VellumPdf.Reader/Fonts/FontCache.cs +++ b/src/VellumPdf.Reader/Fonts/FontCache.cs @@ -18,13 +18,21 @@ namespace VellumPdf.Reader.Fonts; /// 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 +/// 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). Both figures dropped -/// from an earlier measurement (about 9,872 B and 10,495 B respectively) once -/// AdobeGlyphList.TryMapToUnicode stopped routing a single-component glyph name through a -/// StringBuilder, which was most of the per-font cost. +/// 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 this type keeps. /// /// internal sealed class FontCache diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs index 5932a26d..1908259d 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -45,9 +45,11 @@ namespace VellumPdf.Reader.Fonts; /// (), 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 -/// before its type is tested (one hop, a dangling -/// reference resolving to and treated as absent), with one exception: an +/// 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: an /// element of /Differences is read raw (§9.6.5, step 5 below), because §7.3.10 permits an /// indirect reference there and this reader deliberately does not resolve one, recording that as a /// reader limitation () rather than @@ -97,9 +99,12 @@ internal sealed class SimpleFontReader : PdfFontReader private readonly int? _generation; private readonly int? _pageIndex; - private string?[] _names = new string?[256]; - private double[] _widths = new double[256]; - private string?[] _unicode = new string?[256]; + // 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; @@ -176,7 +181,11 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) $"has no usable /BaseFont: {excerpt}."); } - // Step 3: symbolic. + // Step 3: symbolic. A /Flags of the wrong type (Table 121 requires an integer; a real + // is the one a producer is most likely to write by mistake) is treated the same as an + // absent one, silently: this class has four diagnostic codes, all for the font + // dictionary itself, and none of them fits a malformed descriptor entry, so a fifth code + // is not added here for a producer defect this reader has never observed in practice. var descriptor = Resolve(reader, fontDict.Get(_fontDescriptorKey)) as PdfDictionary; var resolvedFlags = descriptor is not null ? Resolve(reader, descriptor.Get(_flagsKey)) as PdfInteger @@ -357,8 +366,8 @@ private static void FillUndefinedFromStandard(string?[] table) private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDict, string?[] table) { var resolved = Resolve(reader, encodingDict.Get(_differencesKey)); - if (resolved is null or PdfNull) - return; // absent (ISO 32000-2 §7.3.9): omitted, a direct null, or a dangling reference. + if (resolved is null) + return; // absent (see this class's Resolve helper): omitted, null, or a dangling reference. if (resolved is not PdfArray differences) { @@ -394,9 +403,18 @@ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDi } if (glyphName.Value.Length > AdobeGlyphList.MaxGlyphNameLength) { - ReportOnce(ref _reported401, PdfReaderDiagnosticCode.FontEncodingMalformed, - $"/Differences names a glyph longer than {AdobeGlyphList.MaxGlyphNameLength} characters: " - + $"{DiagnosticExcerpt.Quote(glyphName.Value)}."); + // 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 @@ -410,8 +428,15 @@ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDi 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 an element this reader does not resolve."); + $"/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 @@ -422,8 +447,8 @@ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDi } // Names a resolved value's type for the 401 message reported when /Differences is present but - // not an array: a name or keyword goes through DiagnosticExcerpt, matching every other Report - // call in this class. + // 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", @@ -499,7 +524,11 @@ private bool BuildWidths(PdfDocumentReader reader, PdfDictionary fontDict, doubl // 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. + // 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) @@ -547,7 +576,21 @@ private void ReportOnce(ref bool flag, PdfReaderDiagnosticCode code, string mess _sink.Report(code, message, _objectNumber, _generation, _pageIndex); } - /// Null-tolerant single-hop resolution through . - private static PdfObject? Resolve(PdfDocumentReader reader, PdfObject? raw) => - raw is null ? null : reader.ResolveValue(raw); + /// + /// 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 goes through this one helper, so that rule + /// applies uniformly to /Encoding, /Widths, /FirstChar, /LastChar, + /// /BaseFont, /FontDescriptor, /MissingWidth, /BaseEncoding, + /// /Flags and /Differences from this one site. + /// + 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 index 767cb519..85c22346 100644 --- a/src/VellumPdf.Reader/Fonts/Standard14Names.cs +++ b/src/VellumPdf.Reader/Fonts/Standard14Names.cs @@ -1,8 +1,6 @@ // Copyright © Timothy van der Ham (@Tim81) // SPDX-License-Identifier: Apache-2.0 -using VellumPdf.Fonts; - namespace VellumPdf.Reader.Fonts; /// @@ -63,22 +61,6 @@ internal static class Standard14Names "Symbol", "ZapfDingbats", }; - private static readonly Dictionary _kernelFonts = new(StringComparer.Ordinal) - { - ["Helvetica"] = Standard14.Helvetica, - ["Helvetica-Bold"] = Standard14.HelveticaBold, - ["Helvetica-Oblique"] = Standard14.HelveticaOblique, - ["Helvetica-BoldOblique"] = Standard14.HelveticaBoldOblique, - ["Times-Roman"] = Standard14.TimesRoman, - ["Times-Bold"] = Standard14.TimesBold, - ["Times-Italic"] = Standard14.TimesItalic, - ["Times-BoldItalic"] = Standard14.TimesBoldItalic, - ["Courier"] = Standard14.Courier, - ["Courier-Bold"] = Standard14.CourierBold, - ["Courier-Oblique"] = Standard14.CourierOblique, - ["Courier-BoldOblique"] = Standard14.CourierBoldOblique, - }; - /// /// 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 @@ -92,7 +74,7 @@ public static bool TryResolve(string baseFont, out string 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.1). + // 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..]; @@ -121,12 +103,4 @@ private static bool IsSubsetTag(string name) } return true; } - - /// - /// Returns the member for one of the 12 text fonts. Returns - /// for Symbol, ZapfDingbats, or any name - /// itself would not have produced. - /// - public static bool TryGetKernelFont(string afmName, out Standard14 font) => - _kernelFonts.TryGetValue(afmName, out font); } diff --git a/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs index 7b68f072..55048cb0 100644 --- a/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs +++ b/src/VellumPdf.Reader/Fonts/SymbolFontMetrics.cs @@ -40,22 +40,33 @@ // 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 diff --git a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs index 6223f5a4..26f48912 100644 --- a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs +++ b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs @@ -16,11 +16,12 @@ namespace VellumPdf.Reader.Fonts; /// /// 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 SymbolFontMetrics' own remarks name as -/// ZapfDingbats.afm-only codes (a85 through a96, a205, a206): -/// they carry ordinary AGL Unicode mappings (the ornamental-bracket block, U+2768–U+2775), and -/// omitting them here would leave the codes that use them (0x80–0x8D) with no Unicode route at -/// all, which SimpleFontReaderTests pins directly against 0x80. +/// 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 at all; omitting them here would leave those codes with +/// no Unicode route at all, which SimpleFontReaderTests pins directly against 0x80. /// internal static class ZapfDingbatsGlyphList { diff --git a/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs index 9b1cecd2..70aa8a27 100644 --- a/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs +++ b/src/VellumPdf.Reader/PdfDocumentReader.Fonts.cs @@ -17,11 +17,15 @@ public sealed partial class PdfDocumentReader /// 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 @@ -36,6 +40,8 @@ public sealed partial class PdfDocumentReader /// internal PdfFontReader? GetFontReader(PdfObject rawFontEntry, DiagnosticSink sink, int? pageIndex) { + ThrowIfDisposed(); + int? objectNumber = null; int? generation = null; if (rawFontEntry is PdfIndirectReference r) diff --git a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs index 3a449b82..3536c943 100644 --- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs +++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs @@ -568,10 +568,10 @@ public enum PdfReaderDiagnosticCode /// 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 building it hit this reader's own indirect-object resolution depth limit - /// ( from PdfDocumentReader.Resolve). 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. + /// 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, @@ -580,9 +580,9 @@ public enum PdfReaderDiagnosticCode /// 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, or was of a type this reader does not resolve (an - /// indirect reference, legal per §7.3.10, is reported under this code as a reader limitation, - /// not a malformation, and stops the array from being applied any further). Reported once per + /// than this reader's own name-length bound, or was of a type §9.6.5.1 does not permit there + /// (only integers and names), including an indirect reference, which §7.3.10 permits but this + /// reader does not follow (stops the array from being applied any further). Reported once per /// font. /// FontEncodingMalformed = 401, diff --git a/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs index bd2b6f76..a1796c21 100644 --- a/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs +++ b/tests/VellumPdf.Conformance.Tests/Fonts/ReaderEncodingParityTests.cs @@ -52,7 +52,8 @@ public void MacRoman_differsAtExactlyTheSeventeenCodes() } // The class doc of both AdobeGlyphList copies asserts the two embedded AdobeGlyphList.txt - // resources are byte-identical; nothing before this test compared the two files. + // resources are byte-identical; this test compares the two files directly rather than + // trusting that claim. [Fact] public void AdobeGlyphListResource_isByteIdenticalAcrossReaderAndConformance() { diff --git a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs index d154729e..41831581 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs @@ -108,6 +108,68 @@ public void TryMapToUnicode_lengthBoundary_underscoreChain() 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() + { + // Pins the round-1 allocation fix directly: a single-component name returns the map's own + // string (or TryUniName/TryUName's own freshly built one) rather than a fresh copy routed + // through a StringBuilder each call, so two calls with the same name share one instance. + 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() { diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs index 865480b3..70d7ad14 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs @@ -12,13 +12,15 @@ namespace VellumPdf.Reader.Tests.Fonts; /// 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, random /Widths lengths and element types, 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. +/// 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 { @@ -43,6 +45,9 @@ internal static long Iterations 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; @@ -51,6 +56,9 @@ private static PdfDocumentReader OpenFixture() => FontTestSupport.Open( 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")); @@ -72,6 +80,20 @@ private static PdfDocumentReader OpenFixture() => FontTestSupport.Open( 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))); + + 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")), @@ -84,26 +106,37 @@ private static PdfDocumentReader OpenFixture() => FontTestSupport.Open( // 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)), - DifferencesGen.Select(diffs => + 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")), - DifferencesGen, + 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))); + 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 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"), diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs index c7915068..4dcd4990 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs @@ -143,7 +143,7 @@ public void Differences_overflowPast255_assignsUpTo255_reports401Once() } [Fact] - public void Differences_unresolvedElementType_reports401WithDoesNotResolveMessage() + public void Differences_unresolvedElementType_reports401WithIndirectReferenceMessage() { using var doc = FontTestSupport.OpenMinimal(); var sink = new DiagnosticSink(50); @@ -156,7 +156,10 @@ public void Differences_unresolvedElementType_reports401WithDoesNotResolveMessag 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.Contains("does not resolve", d.Message); + Assert.Equal( + "/Differences contains an indirect reference, which this reader does not follow " + + "inside /Differences.", + d.Message); } [Fact] @@ -220,10 +223,10 @@ public void Differences_selfReferentialChain_stillAReferenceAfterOneHop_reports4 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. Before the fix this reader resumed - // after the bad element with the running code unchanged, so /zcaron landed on B (0x42) and - // /Zcaron on C (0x43) instead of being skipped; both must keep their StandardEncoding - // names here. + // 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() @@ -238,7 +241,40 @@ public void Differences_badElement_stopsApplyingArray_laterNamesKeepBaseEncoding 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 resolve", d.Message); + 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 number 65).", + 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 ────────────────────────────────────────────────────────────────────── @@ -681,6 +717,22 @@ public void Widths_missingWidthFromDescriptor() 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 (see FillAfmWidths' own remarks); it does not also require a present + // /FontDescriptor or /Flags. Gating it on those would contradict Table 109, which makes + // /FontDescriptor optional for the standard 14 in PDF 1.0 to 1.7, and would break + // Helvetica_noEncoding_noWidths_nonsymbolic above, which has no /FontDescriptor at all. + 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() { @@ -763,7 +815,7 @@ public void NonStandardFont_noWidths_reports402Once_allMissingWidth() Assert.Equal(PdfReaderDiagnosticCode.FontWidthsMalformed, d.Code); } - // ── 12: dangling reference ─────────────────────────────────────────────────────────────────── + // ── 12: dangling reference, null entries ───────────────────────────────────────────────────── [Fact] public void DanglingEncodingReference_treatedAsAbsent_no401() @@ -777,6 +829,80 @@ public void DanglingEncodingReference_treatedAsAbsent_no401() 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] @@ -857,6 +983,16 @@ public void GetFontReader_fontEntryNamesADeepLengthChain_reports400Once_noThrow( 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] @@ -907,13 +1043,52 @@ public void Create_allocatesUnder64KiB_forA100000ElementWidthsArray() Build(doc, fontDict, new DiagnosticSink(50)); var allocated = GC.GetAllocatedBytesForCurrentThread() - before; - // Measured 31,752 bytes on this runtime (the per-font string/width/Unicode tables, the + // 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] diff --git a/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs index 3ac4a635..aba94468 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/Standard14NamesTests.cs @@ -1,7 +1,6 @@ // Copyright © Timothy van der Ham (@Tim81) // SPDX-License-Identifier: Apache-2.0 -using VellumPdf.Fonts; using VellumPdf.Reader.Fonts; namespace VellumPdf.Reader.Tests.Fonts; @@ -74,17 +73,4 @@ public void TryResolve_200CharacterName_false() { Assert.False(Standard14Names.TryResolve(new string('A', 200), out _)); } - - [Fact] - public void TryGetKernelFont_timesRoman_givesKernelEnum() - { - Assert.True(Standard14Names.TryGetKernelFont("Times-Roman", out var font)); - Assert.Equal(Standard14.TimesRoman, font); - } - - [Fact] - public void TryGetKernelFont_symbol_false() - { - Assert.False(Standard14Names.TryGetKernelFont("Symbol", out _)); - } } diff --git a/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs index 56920cf4..fc85b76a 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/ZapfDingbatsGlyphListTests.cs @@ -50,11 +50,13 @@ 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 SymbolFontMetrics' - // own remarks describe as ZapfDingbats.afm-only codes (a85 through a96, a205, a206), which - // Annex D.6 does not document but which do have ordinary AGL Unicode mappings. 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. + // 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); } } From 9bb1cd973731ae33d7de03817a0b065944d3825d Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 13:55:03 +0200 Subject: [PATCH 5/7] test(reader): pin the encoding fill's /Flags gate; doc corrections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The §9.6.5.4 StandardEncoding fill is gated on a resolved /Flags, not on the descriptor's presence (`resolvedFlags is not null && !symbolic`), but no test separated the two: a descriptor without /Flags exercised neither branch. The new TrueTypeWithDescriptorButNoFlags_dictionaryWithMacRomanBase_isNotFilledFromStandard fails when the gate is changed to `descriptor is not null && !symbolic` (1 of 64 SimpleFontReaderTests fails, the rest stay green), so it is the discriminating case for that line. Doc corrections, no behaviour change: the 401 summary named an indirect reference as both permitted and disallowed in one clause and attached the stop-applying note to the wrong antecedent; FontCache's remarks said "every other cache this type keeps" although the type holds one cache (the sentence now points at the reader's caches, as PdfDocumentReader.Pages does); the /Flags comment in SimpleFontReader.Create rested on an unverifiable observation claim; ZapfDingbatsGlyphList's remark said "at all" twice; two test comments described review history rather than the behaviour they pin. --- src/VellumPdf.Reader/Fonts/FontCache.cs | 2 +- .../Fonts/SimpleFontReader.cs | 6 ++--- .../Fonts/ZapfDingbatsGlyphList.cs | 4 +-- src/VellumPdf.Reader/PdfReaderDiagnostic.cs | 8 +++--- .../Fonts/AdobeGlyphListTests.cs | 7 ++--- .../Fonts/SimpleFontReaderTests.cs | 27 ++++++++++++++++--- 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/VellumPdf.Reader/Fonts/FontCache.cs b/src/VellumPdf.Reader/Fonts/FontCache.cs index a28a9bb2..a62ddb5e 100644 --- a/src/VellumPdf.Reader/Fonts/FontCache.cs +++ b/src/VellumPdf.Reader/Fonts/FontCache.cs @@ -32,7 +32,7 @@ namespace VellumPdf.Reader.Fonts; /// 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 this type keeps. +/// thread-safe, like every other cache the reader keeps. /// /// internal sealed class FontCache diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs index 1908259d..70eedb5f 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -183,9 +183,9 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) // Step 3: symbolic. A /Flags of the wrong type (Table 121 requires an integer; a real // is the one a producer is most likely to write by mistake) is treated the same as an - // absent one, silently: this class has four diagnostic codes, all for the font - // dictionary itself, and none of them fits a malformed descriptor entry, so a fifth code - // is not added here for a producer defect this reader has never observed in practice. + // absent one, silently: this class's four diagnostic codes all describe the font + // dictionary itself, none fits a malformed descriptor entry, and none is added, since the + // Table 112 default below leaves the font in a usable state either way. var descriptor = Resolve(reader, fontDict.Get(_fontDescriptorKey)) as PdfDictionary; var resolvedFlags = descriptor is not null ? Resolve(reader, descriptor.Get(_flagsKey)) as PdfInteger diff --git a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs index 26f48912..d8a3b328 100644 --- a/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs +++ b/src/VellumPdf.Reader/Fonts/ZapfDingbatsGlyphList.cs @@ -20,8 +20,8 @@ namespace VellumPdf.Reader.Fonts; /// 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 at all; omitting them here would leave those codes with -/// no Unicode route at all, which SimpleFontReaderTests pins directly against 0x80. +/// 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 { diff --git a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs index 3536c943..35c09b22 100644 --- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs +++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs @@ -580,10 +580,10 @@ public enum PdfReaderDiagnosticCode /// 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, or was of a type §9.6.5.1 does not permit there - /// (only integers and names), including an indirect reference, which §7.3.10 permits but this - /// reader does not follow (stops the array from being applied any further). Reported once per - /// font. + /// 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, diff --git a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs index 41831581..152462a2 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/AdobeGlyphListTests.cs @@ -158,9 +158,10 @@ public void TryMapToUnicode_uniSurrogateBoundary(string name, bool expected) [Fact] public void TryMapToUnicode_singleComponentName_returnsTheSameInstanceEachCall() { - // Pins the round-1 allocation fix directly: a single-component name returns the map's own - // string (or TryUniName/TryUName's own freshly built one) rather than a fresh copy routed - // through a StringBuilder each call, so two calls with the same name share one instance. + // 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); diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs index 4dcd4990..61247124 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs @@ -506,6 +506,25 @@ public void NonsymbolicTrueType_dictionaryWithMacExpertBase_isNotFilledFromStand 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() { @@ -721,10 +740,10 @@ public void Widths_missingWidthFromDescriptor() 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 (see FillAfmWidths' own remarks); it does not also require a present - // /FontDescriptor or /Flags. Gating it on those would contradict Table 109, which makes - // /FontDescriptor optional for the standard 14 in PDF 1.0 to 1.7, and would break - // Helvetica_noEncoding_noWidths_nonsymbolic above, which has no /FontDescriptor at all. + // 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()); From 0523ee94d2235d59c4fce879360d54db4948725c Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 16:00:11 +0200 Subject: [PATCH 6/7] docs(reader): correct the simple-font invariants and citations Round 3 found no behavioural defect. Six prose corrections, of which two change an emitted message and so carry tests. The class remarks said every dictionary entry goes through this class's own Resolve helper "with one exception", and named an element of /Differences. That element is an array element, not a dictionary entry; the real exception is /ToUnicode, whose reference is followed with ResolveStream because Resolve hands back a stream object's dictionary and never the stream itself. Both the class remarks and the helper's own summary now say so, and /Subtype joins the helper's list. The step 3 comment cited Table 121 for /Flags being an integer. Table 121 lists bit meanings and states no type; Table 120 types the entry and 7.3.3 forbids a real where an integer is expected. The same comment called this class's codes four and said all four describe the font dictionary: there are five, of which 400 to 402 describe the dictionary and 403/404 the Unicode routing. Four is the count of report flags, which is what the bounds table means. The comment now also names what an unreadable /Flags costs the producer: step 8's 9.6.5.4 fill does not run, so twelve StandardEncoding cells stay undefined. The 401 message's default arm described a null and a nested array as "a value of a type this reader does not recognise". Both are types this reader defines and handles, so both get their own arm; a real-valued code now reads "the real number 65" rather than printing the integer the producer needed. Removing the two arms fails exactly the two new exact-message tests and nothing else. The 402 doc gains the non-number-element case its own message already named. The ordering rule for /Differences is cited as 9.6.5.1, where it lives. An array opening with a name names no starting code, which 9.6.5.1 requires; the reader starts it at 0 and reports nothing, now documented and pinned. NOTICE read as if the AFM licence conditioned redistribution on the trademark notices as a second condition, and as if it reached derived data. It conditions redistribution of the AFM files themselves on all copyright notices being retained, and a generated width table is not one of those files, so that paragraph now says why the entry exists anyway. The fuzz corpus gains a direct and a one-hop null for /Encoding and for /Differences, so the 7.3.7 normalisation added in round 2 is exercised on more than /Widths. Measured over 60 000 draws of each generator: /Encoding direct null 4859, one-hop null 5046; /Differences direct null 4939, one-hop null 5026. Both properties pass at VELLUMPDF_FUZZ_ITER= 60000 in 1.5 s. Reader 1575/0/12 skipped with oracles required, Conformance 1285/0/0, SimpleFontReaderTests 67 cases over 65 methods. --- NOTICE | 10 +-- .../Fonts/SimpleFontReader.cs | 51 ++++++++++----- src/VellumPdf.Reader/PdfReaderDiagnostic.cs | 2 +- .../Fonts/FontFuzzTests.cs | 11 +++- .../Fonts/SimpleFontReaderTests.cs | 62 ++++++++++++++++++- 5 files changed, 113 insertions(+), 23 deletions(-) diff --git a/NOTICE b/NOTICE index e2537f55..13712375 100644 --- a/NOTICE +++ b/NOTICE @@ -124,10 +124,12 @@ Adobe Core 14 AFM font metrics and that this paragraph is not modified. Adobe Systems has no responsibility or obligation to support the use of the AFM files. - This entry exists because that licence conditions redistribution on the - copyright and trademark notices above being retained; SymbolFontMetrics.cs - is a derived table of glyph names, codes and advance widths, not a copy of - the AFM files themselves. + 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/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs index 70eedb5f..f8de34c5 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -49,11 +49,16 @@ namespace VellumPdf.Reader.Fonts; /// 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: an -/// element of /Differences is read raw (§9.6.5, step 5 below), because §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. +/// 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, step 5 below), 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 @@ -63,10 +68,14 @@ namespace VellumPdf.Reader.Fonts; /// encoding's own glyph at that code, it does not preserve it. /// /// -/// ISO 32000-2 §9.6.5 states the ordering rule for /Differences sequences verbatim: "These +/// 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 @@ -181,11 +190,15 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) $"has no usable /BaseFont: {excerpt}."); } - // Step 3: symbolic. A /Flags of the wrong type (Table 121 requires an integer; a real - // is the one a producer is most likely to write by mistake) is treated the same as an - // absent one, silently: this class's four diagnostic codes all describe the font - // dictionary itself, none fits a malformed descriptor entry, and none is added, since the - // Table 112 default below leaves the font in a usable state either way. + // 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 8'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 @@ -456,9 +469,11 @@ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDi 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 number {r.Value.ToString(CultureInfo.InvariantCulture)}", + 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", }; @@ -581,10 +596,14 @@ private void ReportOnce(ref bool flag, PdfReaderDiagnosticCode code, string mess /// 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 goes through this one helper, so that rule - /// applies uniformly to /Encoding, /Widths, /FirstChar, /LastChar, - /// /BaseFont, /FontDescriptor, /MissingWidth, /BaseEncoding, - /// /Flags and /Differences from this one site. + /// 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) { diff --git a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs index 35c09b22..f3a78fa2 100644 --- a/src/VellumPdf.Reader/PdfReaderDiagnostic.cs +++ b/src/VellumPdf.Reader/PdfReaderDiagnostic.cs @@ -589,7 +589,7 @@ public enum PdfReaderDiagnosticCode /// /// A simple font's /FirstChar, /LastChar, or /Widths (ISO 32000-2 Table - /// 109) was missing, mistyped, out of range, or shorter than + /// 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 diff --git a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs index 70d7ad14..afe6a7f1 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/FontFuzzTests.cs @@ -88,7 +88,11 @@ private static PdfDocumentReader OpenFixture() => FontTestSupport.Open( 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))); + 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), @@ -101,6 +105,11 @@ private static PdfDocumentReader OpenFixture() => FontTestSupport.Open( 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 diff --git a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs index 61247124..73896efe 100644 --- a/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs +++ b/tests/VellumPdf.Reader.Tests/Fonts/SimpleFontReaderTests.cs @@ -256,7 +256,67 @@ public void Differences_realElement_reports401WithExactMessage() 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 number 65).", + "/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); } From f99953545ef6a53e7931e2705e942b3b45e45643 Mon Sep 17 00:00:00 2001 From: Timothy van der Ham Date: Sat, 5 Sep 2026 16:55:45 +0200 Subject: [PATCH 7/7] docs(reader): correct the fill's step number and one citation The step-3 comment named step 8 for the 9.6.5.4 StandardEncoding fill. That fill is step 5, as the same method says seventeen lines below; step 8 is the per-code Unicode mapping, which /Flags does not gate. The overlap comment in ApplyDifferences still cited 9.6.5 for the ordering rule the class remarks already cite as 9.6.5.1, where the sentence lives. The class remarks pointed at "step 5 below" for the raw /Differences element read, which happens in step 4. A class-level doc has to track the body's own numbering to stay right, so it names ApplyDifferences instead. --- src/VellumPdf.Reader/Fonts/SimpleFontReader.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs index f8de34c5..ddca6aa0 100644 --- a/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs +++ b/src/VellumPdf.Reader/Fonts/SimpleFontReader.cs @@ -53,10 +53,9 @@ namespace VellumPdf.Reader.Fonts; /// /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, step 5 below), 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 +/// 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. /// @@ -196,7 +195,7 @@ private void Populate(PdfDocumentReader reader, PdfDictionary fontDict) // 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 8's §9.6.5.4 fill: on a nonsymbolic TrueType with a dictionary + // 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; @@ -433,8 +432,8 @@ private void ApplyDifferences(PdfDocumentReader reader, PdfDictionary encodingDi else { // A later assignment overwrites an earlier one at the same code. ISO - // 32000-2 §9.6.5 forbids overlapping sequences; this reader allows the - // overwrite and reports nothing for it (see the class doc's own remarks). + // 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++;