Skip to content

Move a Plane 2 character into Sun-ExtB only when Sun-ExtB has it (v6) - #847

Merged
jakejackson1 merged 1 commit into
gravitypdf-v6from
fix/837-sip-coverage-v6
Nov 4, 2026
Merged

jakejackson1 merged 1 commit into
gravitypdf-v6from
fix/837-sip-coverage-v6

Conversation

@gravitypdf-bot

@gravitypdf-bot gravitypdf-bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Backports #838 (fixes #837) to gravitypdf-v6.

Stacked on #845. The fix reads Sun-ExtB's coverage through FontSubstitution::widths(), and the ranges files in #845 are what let it answer without loading the font. Merge #845 first; this PR's base then needs to move to gravitypdf-v6.

The bug

When the HTML held a Plane 2 character, the scan put every character above U+1FFFF that the current font lacked into one SIP run, and counted the run as covered with _charDefined($cw, $char) || $char > 131071. The second half was always true, so a CJK Extension E character, or a Plane 3 one, went to Sun-ExtB, which lacks it. Sun-ExtB was found and loaded to draw its missing-character box, and backupSubsFont was never tried. SubstituteCharsSIP() had the same fault for text in a font with a sip-ext font.

The fix

The rule is the same as on gravitypdf:

  • FontSubstitution::sipFontFor($char) names the current font's sip-ext font if it has the character, then backupSIPFont if that has it, otherwise neither. Anything outside Plane 2 gets neither.
  • A character joins a SIP run only when sipFontFor() names a font. Everything else the current font lacks takes the core font and backupSubsFont path. If nothing has it, it stays in the current font.
  • SubstituteCharsSIP() now runs only where the scan doesn't: without useSubstitutions, or for a Type0 font. It takes the first run of Plane 2 characters the sip-ext font has.

Two things change for text in a font with a sip-ext font when substitution is on: the text before a Plane 2 run is now substituted, and a Plane 2 character the font has itself stays in it.

Differences from #838

  • The scan: v6's handles one run per token and has no emoji or colour font handling, so only the SIP parts apply. The two old SIP branches are replaced by one call.
  • moveRun(): new on v6. It cuts the run out by position and writes the text before it back to the token list. The old code found the run again with a regular expression, in a pattern with no s flag.
  • SubstitutionFontCacheTest: this v6-only test used DejaVu Sans as backupSIPFont and asserted U+20000 was drawn in it, which is the bug. It now uses Sun-ExtB and asserts U+20000 is in that font's subsets. Its checks on the characters after U+20000 are unchanged.
  • SipSubstitutionCoverageTest: v6 only includes PHP ranges files shipped for FontVariables families, so the test font's ranges file is written to the font cache as JSON instead. SubstitutionRunSplitTest doesn't exist on v6, so its "a font keeps the Plane 2 text it has" case moved here.
  • Other test changes: TextRecordingMpdf gains drawnFontFamily and drawnPieces(). The sip-coverage fixture is regenerated on v6 and sets backupSubsFont to Sun-ExtA, as the caption says.
  • Left out: SubstitutionRunCutTest (doesn't exist on v6) and the CHANGELOG-GRAVITYPDF.md entry (nor does that file).

Checks

Check Result
PHP 8.5 tests 1,186 tests, 1 skipped
Snapshots with CI=1 all 22 pass, none skipped
composer cs clean
PHPStan 2.3.1 on PHP 8.2 no errors
PHP 5.6 changed source and main test files lint clean; the suite itself is left to CI
  • The snapshot: I rasterised sip-coverage and looked at it. U+20BB7 is in Sun-ExtB, and U+2B820 is DejaVu Sans's box, or blank in the Sun-ExtA paragraph.
  • Without the Mpdf.php change: the snapshot fails, 9 of the 12 SipSubstitutionCoverageTest cases fail, and SubstitutionFontCacheTest errors.
  • With the new scan but the old call sites: 2 coverage cases fail, "Extension E ahead of Extension B" and "Plane 2 a font with a sip-ext font has itself".

There are no benchmarks: this changes which font a character goes to, and isn't a speed change.

Not changed

Plane 3 characters still can't be drawn, on either branch, because TTFontFile reads no cmap entry from U+30000 up; that's #846. A character no font has, left in a font with no sip or smp flag, is still written as two glyphs; that's #839.

@gravitypdf-bot
gravitypdf-bot force-pushed the fix/837-sip-coverage-v6 branch from bd6a135 to 1f31df9 Compare November 4, 2026 22:57
Base automatically changed from feature/827-character-ranges-v6 to gravitypdf-v6 November 4, 2026 22:57
A character the current font lacks joins a SIP run only when it is in Plane 2 and FontSubstitution::sipFontFor() names the font's sip-ext font or backupSIPFont as having it. Anything else, Plane 3 and above included, is offered to the backup fonts like any other missing character, whether or not the document has a Plane 2 character.

With useSubstitutions on, the substitution scan moves Plane 2 into the sip-ext font itself, so a Plane 2 character the current font has stays in it. SubstituteCharsSIP() now runs only where the scan does not, and takes the first run of Plane 2 characters the sip-ext font has through the same check.

Adapted to v6, whose scan handles one run per token and has no emoji handling. The run is cut by position in the new moveRun() rather than with a regular expression. SubstitutionFontCacheTest used DejaVu Sans as backupSIPFont and expected U+20000 to be drawn in it, which it lacks, so it now uses Sun-ExtB.

(cherry picked from commit 2d66b2a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the fix/837-sip-coverage-v6 branch from 1f31df9 to 43c9c9e Compare November 4, 2026 23:05
@jakejackson1
jakejackson1 merged commit c682f90 into gravitypdf-v6 Nov 4, 2026
29 checks passed
@jakejackson1
jakejackson1 deleted the fix/837-sip-coverage-v6 branch November 4, 2026 23:13
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