Skip to content

Fix: resolve AGL underscore ligature glyph names (f_i/f_l/f_f/...) - #36

Merged
Malcolmnixon merged 3 commits into
mainfrom
fix/pdf-type1c-ligature-glyph-names
Oct 4, 2026
Merged

Malcolmnixon merged 3 commits into
mainfrom
fix/pdf-type1c-ligature-glyph-names

Conversation

@Malcolmnixon

Copy link
Copy Markdown
Member

Pull Request

Description

Fixes the still-broken half of the beta.7 real-world glyph-gap report:
/Differences arrays naming a ligature glyph via the Adobe Glyph List's
underscore ligature-naming convention (f_i, f_l, f_f, f_f_i,
f_f_l - two or more component glyph names joined by underscores) were
left unresolved, so the affected glyphs rendered as missing/tofu boxes
even though StandardGlyphNames already had a dedicated codepoint for
the same ligature under its own AGL name (fi, fl, ff, ffi,
ffl). PR #35 already fixed the non-ligature uniXXXX/thinspace
half of this report; this closes the remaining gap.

Adds TryResolveLigatureUnderscoreName: resolves each underscore-
separated component recursively (so a component may itself use the
uniXXXX/uXXXX hex convention), concatenates the resolved characters,
and looks the concatenated string up as an AGL glyph name in
StandardGlyphNames.

The existing real-world text-type1c-differences-agl-ligatures.pdf
fixture already exercised an /f_f ligature in its content stream
(previously documented as a deliberate, out-of-scope limitation); it
now resolves and paints correctly, proving the fix against both a
synthetic regression test and a genuine real-world embedded font.

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

Build and Test

  • Code builds successfully and all tests pass: pwsh ./build.ps1 - all tests passed, 0 failures, 0 regressions
  • Code produces zero warnings

Code Quality

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

Quality Checks

  • All linters pass: pwsh ./lint.ps1 - clean (markdownlint, cspell, yamllint, dotnet format, reqstream, versionmark, reviewmark, sysml2tools)

Testing

  • Added unit tests for new functionality (f_i/f_l/f_f/f_f_i/f_f_l resolving end-to-end against an embedded Type1C font, plus an unrecognized-underscore-name fallback case)
  • Updated existing tests if behavior changed (fixture README/test remarks updated to reflect the formerly out-of-scope /f_f case now resolving)
  • All tests follow the AAA (Arrange, Act, Assert) pattern
  • Test coverage is maintained or improved

Documentation

  • Updated README.md (if applicable) - N/A, no end-user-visible API change
  • Updated docs/ documentation (reqstream/verification docs and fixture README updated)
  • Added code examples for new features (if applicable) - N/A
  • Updated requirements.yaml (if applicable) - N/A, no new top-level requirement; extends existing FontEncoding/Type1CFontResolution requirements

Additional Notes

Follow-up to PR #35 (PDF Type1C/Differences glyph lookup). Queued behind the PPTX Phase 1a-1d work per explicit sequencing instruction.

/Differences arrays naming a ligature glyph via the Adobe Glyph List's
underscore ligature-naming convention (two or more component glyph names
joined by underscores, e.g. f_i for the "fi" ligature) previously left
that code unresolved: neither StandardGlyphNames nor the uniXXXX/uXXXX
hex fallback recognized the underscore-joined spelling, so the affected
glyph rendered as a tofu box even though StandardGlyphNames already had
a dedicated codepoint for the same ligature under its AGL name (fi, fl,
ff, ffi, ffl).

Real-world PDF producers (XeTeX/LuaTeX-derived toolchains in particular)
routinely spell a subsetted font's own ligature glyphs this way. This
was the still-broken half of the beta.7 glyph-gap report on
BR_Cavro_Air_Displacement_Pipettor_396359.pdf pages 4-6 (/fi, /fl, /f_f
ligatures); the non-ligature uniXXXX/thinspace half was already fixed.

Adds TryResolveLigatureUnderscoreName: resolves each underscore-separated
component recursively (so a component may itself use the uniXXXX/uXXXX
hex convention), concatenates the resolved characters, and looks the
concatenated string up as an AGL glyph name in StandardGlyphNames.

The existing real-world text-type1c-differences-agl-ligatures.pdf
fixture already exercised an /f_f ligature in its content stream
(previously documented as a deliberate, out-of-scope limitation); it now
resolves and paints correctly, so this fix is proven against both a
synthetic regression test and a genuine real-world embedded font.

Adds regression tests (f_i/f_l/f_f/f_f_i/f_f_l resolving end-to-end
against an embedded Type1C font, plus an unrecognized-underscore-name
fallback case) and updates the FontEncoding/Type1CFontResolution
ReqStream requirements, verification doc, and fixture README
accordingly.

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

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

Reject surrogate-range component values before UTF-32 conversion to prevent rendering failures.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Adds AGL underscore-ligature resolution for PDF /Differences glyph names.

Changes:

  • Implements recursive ligature component resolution.
  • Adds regression tests and fixture coverage.
  • Updates verification and requirements documentation.
File Summary
test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfFixtures/​README.md Updates fixture behavior documentation.
test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfDocumentTests.cs Adds ligature regression tests.
src/​DemaConsulting.CanvasNet.Pdf/​PdfDocument.Fonts.cs Implements underscore-ligature resolution.
docs/​verification/​canvas-net-pdf/​pdf-document.md Updates verification coverage.
docs/​reqstream/​canvas-net-pdf/​pdf-document.yaml Updates requirements traceability.

💡 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
Addresses PR #36 review finding: TryParseUppercaseHexDigits previously
only rejected values above 0x10FFFF, not the UTF-16 surrogate range
(0xD800-0xDFFF). A lone surrogate is not a valid standalone Unicode
scalar value, so a malformed /Differences glyph name like /uniD800 -
whether standalone or as a ligature component such as /uniD800_i -
would pass codepoint validation but then crash
TryResolveLigatureUnderscoreName's char.ConvertFromUtf32 call with
ArgumentOutOfRangeException instead of failing closed.

Fixed at the shared TryParseUppercaseHexDigits validator (used by both
the uniXXXX/uXXXX hex-name path and, transitively, the ligature path)
so any current or future consumer of resolved codepoints is protected,
not only the ligature resolver.

Added regression tests:
- PdfDocument_Fonts_Differences_SurrogateRangeUniHexName_FallsBackToBaseEncoding
- PdfDocument_Fonts_Differences_LigatureComponentSurrogateRange_FallsBackToBaseEncoding

Updated docs/reqstream and docs/verification for CanvasNetPdf-PdfDocument-FontEncoding.

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

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

Add mixed hex-component test coverage and correct overclaimed fixture verification/documentation.

Review effort: Lite
Findings: None

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

In code that hasn't changed since last review

Medium severity Test hex-named components in the fallback theory

test/​DemaConsulting.CanvasNet.Pdf.Tests/​PdfDocumentTests.cs:7712

The implementation explicitly supports hex-named components (for example uni0066_uni0069), but this theory only exercises literal f/i/l components. Add at least one such case so a regression in the component-to-codepoint fallback cannot pass the entire new test coverage.

Low severity Fixture assertion cannot prove /f_f resolves

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

The verification text says the broad real-world fixture now proves /f_f resolves, but its test only uses AssertPaintedSomePixel while the page also exercises /uni03BC and /thinspace; that assertion cannot distinguish a resolved ligature from a tofu glyph. Please add a glyph-specific check or qualify this statement so the verification record does not claim coverage the test does not provide.

Low severity Fixture test overstates proof of /f_f resolution

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

This updated note overstates what the real-world fixture test proves: PdfDocument_Load_TextType1CDifferencesAglLigaturesFixture_PaintsVisibleGlyphInk only asserts that some pixel is painted, and the same page also contains /uni03BC and /thinspace, so it can pass even if /f_f remains unresolved. Please either add a glyph-specific assertion for the /f_f occurrence or soften this to say that the fixture contains the name; the theory test is the actual isolated coverage today.

…aims

- Add a uni0066_uni0069 InlineData case to
  PdfDocument_Fonts_Differences_LigatureUnderscoreName_ResolvesViaEnrichedEmbeddedFontGlyphMap
  so the theory also exercises AGL-hex-named components (not only
  literal single-letter names), proving
  TryResolveLigatureUnderscoreName's per-component recursion into
  TryResolveGlyphNameToCodepoint, not just a direct StandardGlyphNames
  lookup.
- Correct PdfFixtures/README.md and docs/verification/canvas-net-pdf/pdf-document.md:
  the real-world text-type1c-differences-agl-ligatures.pdf fixture test
  only asserts that some pixel paints somewhere on the page, which
  cannot distinguish a resolved /f_f ligature glyph from an unresolved
  tofu glyph (the page already painted ink before this fix, from
  unrelated surrounding text). The isolated, glyph-specific proof is
  the dedicated synthetic regression test; the fixture now documented
  only as proof the real-world name occurs and the document still
  opens/renders without error.

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

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

One or more issues must be addressed before approval.

Review effort: Lite
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Low severity Keep the explanatory dash inline to preserve Markdown rendering

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

This line starts with - , so Markdown renders the continuation as a new unordered-list item instead of the closing dash in the surrounding sentence. Keep the dash inline with the preceding paragraph (or otherwise indent/escape it) so this explanation renders as a single sentence.

@Malcolmnixon
Malcolmnixon merged commit 762cb3f into main Oct 4, 2026
7 checks passed
@Malcolmnixon
Malcolmnixon deleted the fix/pdf-type1c-ligature-glyph-names branch October 4, 2026 12:40
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