Skip to content

Fix Type1/CFF simple-font glyph lookup for custom /Differences names - #35

Merged
Malcolmnixon merged 6 commits into
mainfrom
fix/pdf-type1c-differences-glyph-lookup
Oct 3, 2026
Merged

Malcolmnixon merged 6 commits into
mainfrom
fix/pdf-type1c-differences-glyph-lookup

Conversation

@Malcolmnixon

Copy link
Copy Markdown
Member

Pull Request

Description

Fixes a glyph-lookup bug in simple fonts (/Type1 with embedded /FontFile
or bare-CFF /FontFile3 /Type1C programs) that custom-encode low character
codes via a /Encoding/Differences array. The synthetic codepoint-to-glyph
lookup built for these embedded font programs was keyed off a generic,
document-independent Adobe-glyph-name guess table, completely disconnected
from the font dictionary's own /Differences-declared glyph names. As a
result, any glyph reachable only through a /Differences override whose
name wasn't in the generic table (for example the AGL uniXXXX convention,
e.g. /uni03BC, or /thinspace, which was missing from the table
entirely), or whose name the embedded font's own CFF charset spelled
differently than the generic guess, silently failed to render.

Root-caused against a real-world reproducer PDF (two subsetted Gotham
Type1C fonts with /Differences remapping codes to /thinspace,
/uni03BC, /fi, /fl, /f_f) and fixed by:

  • Adding an AGL uniXXXX/uXXXX hex-codepoint parsing fallback
    (TryResolveGlyphNameToCodepoint), used by ApplyDifferences.
  • Adding the missing "thinspace" entry to StandardGlyphNames.
  • Reordering BuildResolvedSimpleFont to resolve /Encoding before
    loading the embedded font program, then building a per-font-dictionary
    enriched codepoint-to-glyph-name map (BuildEmbeddedFontGlyphNameMap)
    that lets the document's own literal /Differences-declared name win
    over the generic guess, passed into LoadType1Font/LoadType1CFont.
  • Correcting a stale doc comment that contradicted ApplyDifferences's
    actual (and unchanged) tolerant handling of unrecognized names.

A real-world regression fixture (text-type1c-differences-agl-ligatures.pdf,
extracted and trimmed from the reporter's own PDF) plus targeted
thinspace/uni03BC tests were added. The f_f ligature glyph name
remains an accepted, documented scope boundary (true underscore-ligature
decomposition is a separate concern).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code quality improvement

Related Issues

Closes #

Pre-Submission Checklist

Before submitting this pull request, ensure you have completed the following:

Build and Test

  • Code builds successfully and all tests pass: pwsh ./build.ps1 (7422/7422 passing)
  • Code produces zero warnings

Code Quality

  • New code has appropriate XML documentation comments
  • Static analyzer warnings have been addressed

Quality Checks

Please run the following checks before submitting:

  • All linters pass: pwsh ./lint.ps1 (exit code 0)

Testing

  • Added unit tests for new functionality
  • Updated existing tests if behavior changed
  • All tests follow the AAA (Arrange, Act, Assert) pattern
  • Test coverage is maintained or improved

Documentation

  • Updated README.md (if applicable)
  • Updated docs/ documentation (if applicable)
  • Added code examples for new features (if applicable)
  • Updated requirements.yaml (if applicable) — reqstream verification entries updated

Additional Notes

Also independently verified against an external multi-corpus PDF rendering
harness (pdftest/pypdf/pdf20/payloads/user4, 115 real-world files): 0
Timeout / 0 Crashed, consistent with the pre-fix baseline, and the original
reporter's full reproducer PDF now renders its previously-failing pages
with zero exceptions (remaining exceptions on unrelated pages are a
pre-existing, out-of-scope JPXDecode/JPEG2000 limitation).

Simple fonts with embedded Type1/Type1C (bare CFF) programs built their
synthetic codepoint-to-glyph lookup from a generic, document-independent
Adobe-glyph-name guess table, ignoring the font dictionary's own
/Encoding/Differences-declared glyph names. When a /Differences array
named a glyph via the AGL uniXXXX convention (e.g. /uni03BC) or a name
missing from the generic table (e.g. /thinspace), or when the embedded
font's own CFF charset spelled a glyph differently than the generic
guess, the glyph silently failed to resolve.

- Add TryResolveGlyphNameToCodepoint with an AGL uniXXXX/uXXXX hex
  fallback, used by ApplyDifferences.
- Add the missing "thinspace" entry to StandardGlyphNames.
- Resolve /Encoding before loading the embedded font program in
  BuildResolvedSimpleFont, and build a per-font-dictionary enriched
  codepoint-to-glyph-name map (BuildEmbeddedFontGlyphNameMap) that lets
  the document's own /Differences-declared name win over the generic
  guess, passed into LoadType1Font/LoadType1CFont.
- Correct a stale doc comment claiming unrecognized /Differences names
  throw, matching ApplyDifferences' actual tolerant behavior.
- Add a real-world regression fixture (text-type1c-differences-agl-ligatures.pdf)
  plus targeted thinspace/uni03BC tests.

Verified against the external corpus harness: 0 Timeout/0 Crashed across
all corpora, and the original reporter's reproducer PDF now renders its
previously-failing pages with zero exceptions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 3, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address the remaining glyph-resolution edge cases and add the requested Type 1 and u... regression coverage.

Review effort: Lite
Findings: 2 Medium severity · 2 Low severity

Open (4)
What changed in this PR

Fixes embedded Type 1/Type 1C glyph lookup for custom /Differences names.

Changes:

  • Adds AGL uniXXXX/uXXXX parsing and thinspace support.
  • Enriches embedded-font glyph mappings.
  • Adds regression fixtures, tests, and documentation updates.
File Summary
test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfFixtureTests.cs Adds real-world fixture coverage.
test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfFixtures/​README.md Documents fixture provenance.
test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfDocumentTests.cs Adds targeted regression tests.
src/​DemaConsulting.CanvasNet.Pdf/​PdfDocument.Fonts.Type1.cs Passes enriched mappings to Type 1 loaders.
src/​DemaConsulting.CanvasNet.Pdf/​PdfDocument.Fonts.cs Implements glyph resolution and map enrichment.
docs/​verification/​canvas-net-pdf/​pdf-document.md Records verification coverage.
docs/​reqstream/​canvas-net-pdf/​pdf-document.yaml Updates requirement traceability.
docs/​design/​canvas-net-pdf/​pdf-document.md Updates design documentation.
.cspell.yaml Adds terminology to the spelling configuration.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/DemaConsulting.CanvasNet.Pdf/PdfDocument.Fonts.cs
Comment thread src/DemaConsulting.CanvasNet.Pdf/PdfDocument.Fonts.cs
Comment thread test/DemaConsulting.CanvasNet.Pdf.Tests/PdfFixtureTests.cs Outdated
Comment thread test/DemaConsulting.CanvasNet.Pdf.Tests/PdfFixtures/README.md Outdated
- Reject syntactically-valid-looking u-prefixed glyph names whose parsed
  value exceeds 0x10FFFF (the highest valid Unicode codepoint), e.g.
  /uFFFFFF, instead of treating them as resolved and overwriting the base
  encoding. TryParseUppercaseHexDigits now range-checks the parsed value.
- Add a classic Type 1 (/FontFile) regression test mirroring the existing
  bare-CFF (/FontFile3) /uni03BC coverage, proving the /Differences
  enrichment map also reaches LoadType1Font, not only LoadType1CFont.
  BuildEmbeddedType1FontResources gained a customGlyphName parameter to
  support this.
- Add a boundary regression test for the out-of-range u-prefixed name
  rejection above.
- Correct a test description and a fixture README rationale that both
  overstated which /Differences names fall outside StandardGlyphNames:
  /thinspace is now a direct entry in that table (added by this fix) -
  only /uni03BC needs the AGL uniXXXX hex fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 3, 2026 21:33
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Add end-to-end coverage for valid u names and complete the missing verification, ReqStream, and fixture documentation entries.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Low severity Update README provenance and fixture table for the new PDF

test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfFixtures/​README.md:124

These new paragraphs describe the PDF as an exception, but the README's opening still says every fixture is hand-authored and the fixture table above does not list this filename. Please qualify the opening provenance statement and add text-type1c-differences-agl-ligatures.pdf to the table so the fixture catalog is internally consistent.

Comment thread src/DemaConsulting.CanvasNet.Pdf/PdfDocument.Fonts.cs
…ME provenance

Adds a Theory covering u1234/u10000/u10FFFF (4/5/6-digit AGL hex glyph names)
resolving end-to-end via an embedded Type1C font, addressing review feedback that
only the out-of-range rejection path was previously tested.

Also qualifies the PdfFixtures README's opening and third-party-corpus provenance
statements and adds the missing text-type1c-differences-agl-ligatures.pdf row to
the fixture table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 3, 2026 22:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address the unresolved U+0000 handling issue and documentation traceability/provenance gaps.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
Previously missed (2)

In code that hasn't changed since last review

Low severity Add classic FontFile regression test to Type1 verification list

docs/​reqstream/​canvas-net-pdf/​pdf-document.yaml:971

The new classic /FontFile regression test PdfDocument_Fonts_Differences_Uni03BCName_ResolvesViaEnrichedEmbeddedType1FontGlyphMap is not present in this requirement's Type1 tests list. The requirement prose now covers the enriched map, but this path is not traceable to verification; add the test to the Type1FontResolution list and its verification section.

Low severity Add classic FontFile test to Type1 verification coverage

docs/​verification/​canvas-net-pdf/​pdf-document.md:1210

This verification section lists only the Type1C test and the real-world fixture, but the PR also adds PdfDocument_Fonts_Differences_Uni03BCName_ResolvesViaEnrichedEmbeddedType1FontGlyphMap for the classic /FontFile path. Add that test to the Type1FontResolution verification list; otherwise the newly documented classic-path coverage is omitted from the verification traceability.

Comment thread test/DemaConsulting.CanvasNet.Pdf.Tests/PdfFixtures/README.md Outdated
…e provenance

Adds PdfDocument_Fonts_Differences_Uni03BCName_ResolvesViaEnrichedEmbeddedType1FontGlyphMap
to the Type1FontResolution reqstream tests list and verification document, which
previously traced only the Type1C/CFF variant.

Corrects the PdfFixtures README's provenance/licensing overview: it previously
claimed text-type1c-differences-agl-ligatures.pdf was the only third-party
exception and that the entire corpus is MIT-licensed, overlooking that
text-embedded-truetype-font.pdf and text-composite-truetype-identity-h.pdf also
embed real, SIL Open Font License-licensed Open Sans bytes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 3, 2026 23:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved glyph-mapping issues and incomplete test traceability and fixture provenance documentation remain.

Review effort: Lite
Findings: None

Resolved since last review (3)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Handle zero-valued glyph mappings without treating them as undefined

src/​DemaConsulting.CanvasNet.Pdf/​PdfDocument.Fonts.cs:635

The new parser reports /uni0000 and /u0000 as successfully resolved, but ResolveEncoding uses codepoint 0 as its undefined-slot sentinel and drops any entry whose value is zero (lines 474-480). Thus these newly accepted AGL names silently remain unmapped instead of reaching the embedded glyph. Either support an explicit defined/undefined state in the encoding table or reject zero consistently in this parser.

Low severity Correct PdfFixtures summary to reflect real-world fixture provenance

test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfFixtureTests.cs:260

The class-level XML summary still says the entire PdfFixtures corpus is hand-authored, while this newly added test deliberately opens the real-world excerpt described here. Update that summary so the test documentation does not contradict the fixture provenance.

The class-level summary still claimed the entire PdfFixtures corpus is
hand-authored, contradicting the test immediately below that deliberately opens
the real-world text-type1c-differences-agl-ligatures.pdf excerpt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 3, 2026 23:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved glyph-resolution edge cases and fixture provenance/documentation issues remain.

Review effort: Lite
Findings: None

Previously missed (2)

In code that hasn't changed since last review

Medium severity U+0000 mappings are lost due to undefined sentinel handling

src/​DemaConsulting.CanvasNet.Pdf/​PdfDocument.Fonts.cs:634

This parser accepts uni0000/u0000 as a valid codepoint (the parsed value is 0), but ResolveEncoding later uses 0 as the undefined sentinel and drops table entries whose value is 0. Consequently the new fallback silently fails for a valid U+0000 glyph name; either preserve an explicit mapped-zero state through encoding resolution or reject U+0000 consistently with the representation.

Low severity Third-party fixture lacks verifiable provenance and licensing

test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfFixtures/​README.md:139

Unlike the repository's other third-party fixture documentation, this entry identifies only a reporter's document and provides no source URL, authorship, license text, or sidecar provenance record. Since the PDF bytes are redistributed in the test package, add verifiable provenance/licensing here (as SvgFixtures/README.md does) or keep the regression fixture synthetic.

@Malcolmnixon
Malcolmnixon merged commit 3498c3e into main Oct 3, 2026
7 checks passed
@Malcolmnixon
Malcolmnixon deleted the fix/pdf-type1c-differences-glyph-lookup branch October 3, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants