From 5a88a364bf4706b18fc327b8e9a18ef99e6c7383 Mon Sep 17 00:00:00 2001 From: sebastien Date: Wed, 9 Sep 2026 00:48:40 +0200 Subject: [PATCH] Stop rejecting BaseFontBlend as a Multiple Master CFF font Top DICT operator 12 23 is BaseFontBlend, not a Multiple Master marker. It is a delta array left behind by Multiple Master tooling; it carries no blend axes and no interpolation state, and the glyphs of a font holding it are exactly the ones in its CharStrings INDEX. A real PDF carries a font whose Top DICT has 12 23 with the values [408, -397] -- a plain delta -- and the whole document failed to render because of it. 12 20 SyntheticBase still rejects, and that one is substantive: those glyphs are defined against another font and cannot be read from this CharStrings INDEX alone. The message no longer claims Multiple Master support is the issue. The new case parses the same fixture twice, with and without BaseFontBlend, and asserts identical glyph names and identical outline commands -- so the entry is proven inert rather than merely tolerated. Refs #2 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test-native-cff-semantics.mjs | 27 ++++++++++++++++++++++++--- src/pdf/nativeCff.ts | 8 ++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/scripts/test-native-cff-semantics.mjs b/scripts/test-native-cff-semantics.mjs index 95cee66..07f90e2 100644 --- a/scripts/test-native-cff-semantics.mjs +++ b/scripts/test-native-cff-semantics.mjs @@ -64,6 +64,23 @@ function testCffStructureAndType2Outlines() { assert.deepEqual(flex.bounds, [0, 0, 90, 50]); } +function testBaseFontBlendIsNotMultipleMaster() { + // Top DICT 12 23 is BaseFontBlend, a delta left behind by Multiple Master + // tooling. It carries no interpolation state and no blend axes: the glyphs + // stay exactly the ones in CharStrings. Rejecting it as a Multiple Master + // font discards documents whose outlines are perfectly ordinary. + const baseline = NativeCffFont.parse(buildCffFixture()); + const blended = NativeCffFont.parse(buildCffFixture({ + topDictExtra: concat(dictInteger(408), dictInteger(-397), Uint8Array.of(12, 23)) + })); + assert.equal(blended.numGlyphs, baseline.numGlyphs); + assert.deepEqual(blended.glyphNames, baseline.glyphNames); + assert.deepEqual( + blended.getGlyphOutline(1).commands, + baseline.getGlyphOutline(1).commands + ); +} + function testFontMatrixNormalization() { const font = NativeCffFont.parse(buildCffFixture({ fontMatrix: [0.002, 0, 0, 0.003, 0.01, -0.02] @@ -224,7 +241,8 @@ function buildCffFixture(options = {}) { assert.equal(privateDictionary.length, 18); const localSubrsIndex = cffIndex(localSubrs); const fontMatrix = options.fontMatrix ?? null; - const placeholderTop = topDictionary(0, 0, 0, privateDictionary.length, 0, fontMatrix); + const topDictExtra = options.topDictExtra ?? new Uint8Array(); + const placeholderTop = topDictionary(0, 0, 0, privateDictionary.length, 0, fontMatrix, topDictExtra); const placeholderTopIndex = cffIndex([placeholderTop]); const prefixLength = header.length + nameIndex.length + placeholderTopIndex.length + stringIndex.length + globalSubrsIndex.length; @@ -238,7 +256,8 @@ function buildCffFixture(options = {}) { charStringsOffset, privateDictionary.length, privateOffset, - fontMatrix + fontMatrix, + topDictExtra ); assert.equal(top.length, placeholderTop.length); @@ -257,12 +276,13 @@ function buildCffFixture(options = {}) { ); } -function topDictionary(charset, encoding, charStrings, privateSize, privateOffset, fontMatrix) { +function topDictionary(charset, encoding, charStrings, privateSize, privateOffset, fontMatrix, extra = new Uint8Array()) { const matrix = fontMatrix === null ? new Uint8Array() : concat(...fontMatrix.map((value) => dictReal(value)), Uint8Array.of(12, 7)); return concat( matrix, + extra, dictInteger(charset), Uint8Array.of(15), dictInteger(encoding), Uint8Array.of(16), dictInteger(charStrings), Uint8Array.of(17), @@ -381,6 +401,7 @@ function expectPdf(callback, code, message) { testCffStructureAndType2Outlines(); testFontMatrixNormalization(); +testBaseFontBlendIsNotMultipleMaster(); await testPdfFontSelectionIsSeparateFromToUnicode(); testMalformedProgramsAndLimits(); testDeprecatedPdfDotsectionCompatibility(); diff --git a/src/pdf/nativeCff.ts b/src/pdf/nativeCff.ts index b52c4cc..a0e481a 100644 --- a/src/pdf/nativeCff.ts +++ b/src/pdf/nativeCff.ts @@ -170,9 +170,13 @@ export class NativeCffFont { "cff-cid-not-supported" ); } - if (top.has(dictOperator(12, 20)) || top.has(dictOperator(12, 23))) { + // 12 20 is SyntheticBase: those glyphs are defined against another font and + // cannot be read from this CharStrings INDEX alone. 12 23 is BaseFontBlend, + // a delta left behind by Multiple Master tooling that carries no blend axes + // and no interpolation state, so a font holding it is read normally. + if (top.has(dictOperator(12, 20))) { throw cffUnsupported( - "Synthetic or Multiple Master CFF fonts are not supported.", + "Synthetic CFF fonts are not supported.", "cff-synthetic-not-supported" ); }