From 91f2facce3b7784a8d7af9161575045a02d601d6 Mon Sep 17 00:00:00 2001 From: calixteman Date: Mon, 29 Dec 2025 14:54:54 +0100 Subject: [PATCH] Use the CFF program directly for CID fonts wrapped in OpenType FontFile3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a CIDFontType0 descendant has its program in a FontFile3 stream with /Subtype /OpenType, the OTF wrapper sometimes lacks a usable cmap and the CID→GID mapping only exists inside the embedded CFF itself. In that case the OpenType-table path produces wrong glyphs, so route the font through CFFFont and let it consume the inner CFF directly. The file has been found in https://issues.chromium.org/issues/471404119. --- src/core/evaluator.js | 18 ++++++++++++++++-- src/core/fonts.js | 31 ++++++++++++++++++------------- test/pdfs/.gitignore | 1 + test/pdfs/Embedded_font.pdf | Bin 0 -> 3323 bytes test/test_manifest.json | 7 +++++++ 5 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 test/pdfs/Embedded_font.pdf diff --git a/src/core/evaluator.js b/src/core/evaluator.js index e1badcec3670b..4f33989d0757e 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -4664,9 +4664,22 @@ class PartialEvaluator { throw new FormatError("invalid font name"); } - let fontFile, subtype, length1, length2, length3; + let fontFile, fontFileN, subtype, length1, length2, length3; try { - fontFile = descriptor.get("FontFile", "FontFile2", "FontFile3"); + fontFile = descriptor.get("FontFile"); + if (fontFile) { + fontFileN = 1; + } else { + fontFile = descriptor.get("FontFile2"); + if (fontFile) { + fontFileN = 2; + } else { + fontFile = descriptor.get("FontFile3"); + if (fontFile) { + fontFileN = 3; + } + } + } if (fontFile) { if (!(fontFile instanceof BaseStream)) { @@ -4776,6 +4789,7 @@ class PartialEvaluator { name: fontName.name, subtype, file: fontFile, + fontFileN, length1, length2, length3, diff --git a/src/core/fonts.js b/src/core/fonts.js index 4f5932d318571..744501a2eacb3 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -2670,10 +2670,23 @@ class Font { } const isTrueType = !tables["CFF "]; + let parsedCff = null; if (!isTrueType) { + try { + parsedCff = new CFFParser( + new Stream(tables["CFF "].data), + properties, + SEAC_ANALYSIS_ENABLED + ).parse(); + } catch { + warn("Failed to parse font " + properties.loadedName); + } + // OpenType font (skip composite fonts with non-default glyph mapping). if ( - (header.version === "OTTO" && !properties.composite) || + (header.version === "OTTO" && + (!properties.composite || + (properties.fontFileN === 3 && parsedCff?.isCIDFont))) || !tables.head || !tables.hhea || !tables.maxp || @@ -2713,19 +2726,11 @@ class Font { } let numGlyphsFromCFF; - if (!isTrueType) { + if (parsedCff) { try { - // Trying to repair CFF file - const parser = new CFFParser( - new Stream(tables["CFF "].data), - properties, - SEAC_ANALYSIS_ENABLED - ); - const cff = parser.parse(); - cff.duplicateFirstGlyph(); - const compiler = new CFFCompiler(cff); - tables["CFF "].data = compiler.compile(); - numGlyphsFromCFF = cff.charStringCount; + parsedCff.duplicateFirstGlyph(); + tables["CFF "].data = new CFFCompiler(parsedCff).compile(); + numGlyphsFromCFF = parsedCff.charStringCount; } catch { warn("Failed to compile font " + properties.loadedName); } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index a0c521f326746..bc6027dc6a5b4 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -921,3 +921,4 @@ !operator_list_cycle.pdf !knockout_groups_test.pdf !issue18032.pdf +!Embedded_font.pdf diff --git a/test/pdfs/Embedded_font.pdf b/test/pdfs/Embedded_font.pdf new file mode 100644 index 0000000000000000000000000000000000000000..823909f92a813180ac5e1c53789f5cb2898305d3 GIT binary patch literal 3323 zcma)83se->88*g8GFVBaN2z$swTghk?#|4<#v&FM2q3SptLvK%v%~HZc4j*}C?pY$ zCP!mMjA?CpG}y*hAd1jNR1_rDC<0NVR%6hll?X95_(obK^uMz#tdP^w*|X=|`R9M% z?|+15#3e+LDgzOEz4hz`fJ6kM(FBJ^e1(A>GOF&n13C zXf-4vNh5L*dXh0J$2{**%NR+Nr!e#$9fXw-2-1KJ^l4TOpi)mUh}P<`A~=Br1;S>m z%AbxoFj)&aW?KkSZ%{D?T4P`oHGZ!orPHZ&q=AGaX*D`VMeAszkwF$0;g@JqMeFqx zZ9tYBaLFPO(Xk9k#vn^B5f9oaqXvXYEjUH#DXsU6&AN_D zPQjkXV~P)IPa(s^%50u!_q%F?pCS|7t?U$qI9BFSc-#!gF+)-`M5TdeVFaj@L|Z<` zODG&FLY3=55|7_)Naj5<;#dzK;inrT%je{5_@;nQ9jiRk(?nS`vx3KBii+c1&J?HJ z0shDF9uC063NmK)z!ifI0Cv28t?@qNUt+E zu^u-I*Ut)eo`}Yvh(XZ_@F?wU!1rFM>>SR)_F+Am(d)GecP||-TM4hvDK1~a9ZkuB z;wpB*De!%1a*J-F$Vcm(mQaT!6@yIBO)M(*bLLO^}=|g$_o$VbT@C|f(IVgsLCW`gIAi@46IP-WK zk>0VRP26-|D(;8|SH-m>%qM}>vF}e^J^tg$q+Edaesk?Z+Z!{?&o9{=wBB3Eceka~AG>yfXjLgk3KMU33g}te@t&JwCj(dDXjX z%977>&7WJF?oYo*e^vQe%kQh3Dhj&JZ9JYdxx4AgE8~vUEvxIuyme#avsX^<2x=|9 zu)L_~_QUSh$AKLo_JF2?lS*p$oiVqkzsW__26k8GP|+7pch>FPxopILvaVLOA8zhU z$<7F2Pi5|Rw5&Kh^VEovjVYsFyka0vPMjs(dEc4Uw)kF?u05;ghke`An=5x8aW}Sv ze0g-)g|@fHNfF%Tk@ho73tE49i72|&ePSY6+g4se)D^dNS6@C;y>-EBjSrtR9l7ww zU(L9G;vd{`F2WSj!5u4qG@+sHCcS5I`@}n?!s7=OmuIA<&uy|#?5=91tG8XeRq^q6 zUHs&ERgLBCe<*IdS^o8fId=>-!4nRdO3QaG{igkgO5v_MZ0&~D1E)rHv_6R#Z3r-I zko3*9HBZXoA9aryMbFvY_1mkd)}ESxzjsFNshQT`e)s39)PF~Q_15TnW7E6puWE-p zTKeajFHV+)mIMU8wW7#0^Lf7D8R|jnd(N%L$ZYbU>>Sv6_=FLsO_ znK&i)``1&Rly51GaLsex5)x~EH@SXHWZC)e(UG;g*8JKOIkugTd~I@iPE2Oi_J4i1 z^6d7s*Hgb9yfhX_Fmg1A)Dw8Xcps$K>1Z9wL47pbr0|1;ei{SI!T=h{z#=e!MrjAr zV96Q8r`N#(GeFir54K^XprsGsV+@1*gQg7Q<7qxps~cpO)Zztm03Tjc2hsE}76$M! zusHV7WQm1_4zm~beY`XvyzIgF$`nNz;SCbbX6LK0E$O{m3M