From 1f8eed020f0cbbe2736ad06cdfcd21b8a4c83294 Mon Sep 17 00:00:00 2001 From: calixteman Date: Wed, 20 May 2026 21:41:10 +0200 Subject: [PATCH 1/2] Keep the first /Subrs and /CharStrings block Some Type1 fonts (the embedded Optima variants in orw1972.pdf) ship two /Subrs and /CharStrings blocks wrapped in save/restore frames gated on an Adobe hires/lores runtime switch. In such cases, we just use the first /Subrs and /CharStrings block, which is the one that is actually used by the font renderer in Acrobat. It fixes #18548. --- src/core/type1_parser.js | 15 +++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/issue18548_reduced.pdf | Bin 0 -> 3271 bytes test/test_manifest.json | 7 +++++++ 4 files changed, 23 insertions(+) create mode 100644 test/pdfs/issue18548_reduced.pdf diff --git a/src/core/type1_parser.js b/src/core/type1_parser.js index 96343fbe8e285..843e671ad013b 100644 --- a/src/core/type1_parser.js +++ b/src/core/type1_parser.js @@ -566,6 +566,13 @@ class Type1Parser { }, }; let token, length, data, lenIV; + // Some fonts (e.g. those embedded in issue18548.pdf) define a second + // `/Subrs` and `/CharStrings` block that the PostScript runtime selects + // conditionally (e.g. high-resolution variants). Testing with other + // viewers shows that none of them actually use these conditional blocks, + // so we can "safely" ignore them. + let subrsParsed = false; + let charStringsParsed = false; while ((token = this.getToken()) !== null) { if (token !== "/") { continue; @@ -573,6 +580,10 @@ class Type1Parser { token = this.getToken(); switch (token) { case "CharStrings": + if (charStringsParsed) { + break; + } + charStringsParsed = true; // The number immediately following CharStrings must be greater or // equal to the number of CharStrings. this.getToken(); @@ -610,6 +621,10 @@ class Type1Parser { } break; case "Subrs": + if (subrsParsed) { + break; + } + subrsParsed = true; this.readInt(); // num this.getToken(); // read in 'array' while (this.getToken() === "dup") { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index bc6027dc6a5b4..16ebf27c2969e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -922,3 +922,4 @@ !knockout_groups_test.pdf !issue18032.pdf !Embedded_font.pdf +!issue18548_reduced.pdf diff --git a/test/pdfs/issue18548_reduced.pdf b/test/pdfs/issue18548_reduced.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b38274d7c407e75b1e717c34c2f974802676cf63 GIT binary patch literal 3271 zcmeHKdr%YC8gJ`Vaf@0CS5U6)5kx~ZS!q!pI7;^s@?XIYBILq(Q2L6;Fp&NC&|H(FjF$Wvn9WhQgqz+mB!gX2el7nF~^d zP+l(N{?J|mMw1T800Uwp)sRqwdl3N{>k;~4f6y(#Kx;IR4snOWX6a$?aTsA{EI=w_ zqoWxLLv4s!2d#>L2s+f6VBD;C5s(OhQ~^0Qz*E5|80fM3TpAqHV$*)Smi zGKpjapo3<{C-PNrWI4dWi5G^0QQ1NsA`$^3ZeTzrX2uDGiz|jOk#MRww$Wi9Udq8J z8Yc3v!lXiNAeHxz$F)t;snD@wa}d{ zE6&ldc5L=6y#!?_oZYoTghKc3@#HQ254>^Eathrz8HU!HQG!o~T`ZFWl%mi)gx!?H zG8u4+08SXOT!7^oEI<%KkPHk3*d5Fow(JPoH#x+Hie(|sG|S+i*bSF!FDwNzM4MPN zVpuL_KxjVsVS-n~t(by;R;pJ8C{Wie%Yx^6y5@c|-^XvzcA;(;a=-DL>(@Wde&_rS zmrFM(c4}kMPw|7q=cg_e9^ez1Crj@IH&&>ls-21=yVrZ)UAVX5K=kp_nRhSRG|_RB zAMmGjd9;02^+n^uPm2$$yDW9>QA7cGP4BG)4V~{KrhC7ra5hd{p}##CcQCDbYGXBT z?=o%D#wI_kZPn_JI$TLD8hNuHPrvq7#p3MGO=}Bk^RF&Bx%CIHvYdb`H@!;FR62{d z<`n?Z&zgVGmHdI1T7tj-*%xDDu|!8SMiOeUvidwq#jI{{&lEQOf0T zt=~~s(PPhFoB`tH+UB12!y1ptU0G-D^eJyg&i(r5-YJz$J|72fn&wpzar%B-#01fv z*38Zwc$!+Pjp>%=q^^GLWc+KP>H7RLTGcD5<;QBhs;Jfzd&x&vDh`G7Y6{=Jw)j%X zv6X0O%kIZINpF+?hJ)pg*}v1RQ~bC-o@jH~ij4zA!umJgP9^tHXc9$0tIDr%%71M3=c8zc|J zihy}nXDIY*`NQL-3-?r?TkBOm{@k{dpaOL8`1zMg_gh`srI$V5l4l%p_WgeR`ImiF5gU|t1fE0x(er-;Lg9Zy4GdNcr5MtNQrXDZ8)+^D zPbPR_8IdVG5M&Y=Fo7pL5xlpu4F-9&- uJVKYDPp)?c;3W|PHgZvvPSszva literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index e3d03daa37e93..9e3ec7d87c784 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -14267,5 +14267,12 @@ "md5": "b68dd5a3e6833d1af94e295fe1d60285", "rounds": 1, "type": "eq" + }, + { + "id": "issue18548_reduced", + "file": "pdfs/issue18548_reduced.pdf", + "md5": "39d15f7f810bd89a4e5858df9c75ca4e", + "rounds": 1, + "type": "eq" } ] From adcde1175e0947ffcd0da3a4c655731403be567a Mon Sep 17 00:00:00 2001 From: calixteman Date: Sat, 23 May 2026 22:59:28 +0200 Subject: [PATCH 2/2] Substitute a system font when an embedded CFF is truncated It fixes #7625. If the Top DICT's Private DICT extends past the end of the font data, the Local Subrs INDEX is unreachable and every CharString that calls a subr ends up as a blank glyph. Throw from parsePrivateDict so the existing catch in translateFont triggers fallbackToSystemFont, then run getFontSubstitution post-construction so we pick a close local match instead of the generic fallbackName. --- src/core/cff_parser.js | 6 ++++++ src/core/evaluator.js | 30 +++++++++++++++++++++++++++++- test/pdfs/issue7625.pdf.link | 1 + test/test_manifest.json | 10 ++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/pdfs/issue7625.pdf.link diff --git a/src/core/cff_parser.js b/src/core/cff_parser.js index c6655de89ac59..2a8935dd83e7c 100644 --- a/src/core/cff_parser.js +++ b/src/core/cff_parser.js @@ -787,6 +787,12 @@ class CFFParser { this.emptyPrivateDictionary(parentDict); return; } + // The Private DICT extends past the end of the font data, which means + // the embedded font is truncated; abort so the caller can substitute a + // system font instead of rendering blank glyphs (issue 7625). + if (offset + size > this.bytes.length) { + throw new FormatError("CFF Private DICT extends past end of font"); + } const privateDictEnd = offset + size; const dictData = this.bytes.subarray(offset, privateDictEnd); diff --git a/src/core/evaluator.js b/src/core/evaluator.js index d0cba1ddf55ce..6961d2b0021e0 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -4728,7 +4728,35 @@ class PartialEvaluator { const newProperties = await this.extractDataStructures(dict, properties); this.extractWidths(dict, descriptor, newProperties); - return new Font(fontName.name, fontFile, newProperties, this.options); + const font = new Font(fontName.name, fontFile, newProperties, this.options); + // The embedded font may have been too corrupt to parse, in which case + // we ended up in the fallback path without a substitution selected. + // Try the substitution map now so text renders in a font close to what + // the document asked for (issue 7625). + if ( + font.missingFile && + !font.systemFontInfo && + !isType3Font && + this.options.useSystemFonts + ) { + const standardFontName = getStandardFontName(fontName.name); + const substitution = getFontSubstitution( + this.systemFontCache, + this.idFactory, + this.options.standardFontDataUrl, + fontName.name, + standardFontName, + type + ); + if (substitution) { + if (substitution.guessFallback) { + substitution.guessFallback = false; + substitution.css += `,${font.fallbackName}`; + } + font.systemFontInfo = substitution; + } + } + return font; } static buildFontPaths(font, glyphs, handler, evaluatorOptions) { diff --git a/test/pdfs/issue7625.pdf.link b/test/pdfs/issue7625.pdf.link new file mode 100644 index 0000000000000..4d75563e892ee --- /dev/null +++ b/test/pdfs/issue7625.pdf.link @@ -0,0 +1 @@ +https://github.com/mozilla/pdf.js/files/467169/Er.aestetik.en.loftestang.for.laering.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 8fc98c0187706..9c9153bdc3972 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -14310,5 +14310,15 @@ "md5": "39d15f7f810bd89a4e5858df9c75ca4e", "rounds": 1, "type": "eq" + }, + { + "id": "issue7625", + "file": "pdfs/issue7625.pdf", + "md5": "77ca0a41da767dca31ca45219e2ae202", + "link": true, + "rounds": 1, + "firstPage": 1, + "lastPage": 1, + "type": "eq" } ]