From 5a7a44db7a7cba537f1071d4eea848d015961a11 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 9 Nov 2026 09:26:00 +0100 Subject: [PATCH] Fix the measurement of b and i elements in XFA rich text `TextMeasure` has no `pushFont` method, so laying out any rich text containing a or an threw and no page was rendered. It fixes #22023. --- src/core/xfa/xhtml.js | 11 ++--- test/pdfs/.gitignore | 1 + test/pdfs/xfa_issue22023.pdf | Bin 0 -> 2674 bytes test/test_manifest.json | 9 ++++ test/unit/xfa_tohtml_spec.js | 87 +++++++++++++++++++++++++++++++++++ web/xfa_layer_builder.css | 8 ++++ 6 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 test/pdfs/xfa_issue22023.pdf diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js index 788e8c762ac26..edc8ff630d606 100644 --- a/src/core/xfa/xhtml.js +++ b/src/core/xfa/xhtml.js @@ -236,8 +236,7 @@ class XhtmlObject extends XmlObject { } } - [$pushGlyphs](measure, mustPop = true) { - const xfaFont = Object.create(null); + [$pushGlyphs](measure, mustPop = true, xfaFont = Object.create(null)) { const margin = { top: NaN, bottom: NaN, @@ -374,9 +373,7 @@ class B extends XhtmlObject { } [$pushGlyphs](measure) { - measure.pushFont({ weight: "bold" }); - super[$pushGlyphs](measure); - measure.popFont(); + super[$pushGlyphs](measure, /* mustPop = */ true, { weight: "bold" }); } } @@ -464,9 +461,7 @@ class I extends XhtmlObject { } [$pushGlyphs](measure) { - measure.pushFont({ posture: "italic" }); - super[$pushGlyphs](measure); - measure.popFont(); + super[$pushGlyphs](measure, /* mustPop = */ true, { posture: "italic" }); } } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 65b515800a631..ff657d28d8431 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -1065,3 +1065,4 @@ !issue19954_reduced.pdf !issue1936_reduced.pdf !issue22462.pdf +!xfa_issue22023.pdf diff --git a/test/pdfs/xfa_issue22023.pdf b/test/pdfs/xfa_issue22023.pdf new file mode 100644 index 0000000000000000000000000000000000000000..18cc3c5a1849bfab8f014c9fade44245e19cf937 GIT binary patch literal 2674 zcma)830PBC78VADhKN)V1!a0wz(h(`k`NapKv2pe4N)91N|u+95b|Q)1A#%S78Npx z3zZg%C@!dg3s}^FFle!&RdyA@R?A{hDvpS)%4nVYf>?!q}6~GPg&{UX+BRWit>M%g;CkXHpnqT~+7_tm85MKZt`hH6@ z1W7=&z#I7Z4E;!OO9Bt!_FY^INaJK$gbH|OC=@xWQ>s-|$TtC=f+KNyEewnb_(K9c zis7#g- zdVyo`S3dG2tS;>bqtN}HvG&-)%)#p_kRV5J>r1wg&n>1dap4hpf9>ma=@7IJ;~Xk! zb6V&T@iN?8;e?P0=wZ@0ShHqw=|qJNX%TA#SIecaNOdq6qPppkYMlTU_8Y+;q!7do zJ9|wE)64t)thoF`VD~w22zZ*m>BWM_=QY>U9wu*W4_!;SxyykudD`~X?UzGXmreJ# z)Kp!nHSwmItnz2sclGQ$_SxdI+or6jX;l4K(8}HH+KyJccQ|#mvwl9a@;)Oc#V+&E zMvL?tIk94zz)~pWAHJEf{7l;AwctOx@>C5~S@22Nv~64R3s3DYpA$T%`RKW7?~lU-qhn!G@Y-AT0`-R3f|!BXZb;sZDj53gO2Gg z+McFrRZC&qoEdi6%G%qtSDkmC+Dx6|FKIg&w3f+zI%eG+mH!NOPq2+1w>oSxwihsk<2QRpW~S z-y*D8maez5`LZ}OWoqn+GS0VD-=mVS27lLi7tw2TN+=_Q_odO%+oY)jrUfF*rZXA%Fdft{|+{Bmd=G#P!HZ z$@cm%$A$ftreeqQ<*S-zEj@ncV7a0%rFl(xgx{t;B^lW}ts1_G8)!dSrK0h9P96QT zjNMZf#P<1)&JuBSM`Ve^=Da^kKQ0YLZ!ySKPa|zA8jO<;3Q29>Z8;vJ6SnNJZBxpm zyO2xxg9okK4G&VH$8K?{%%U2Xq@*|?>Cv$V?3bgV*Ii8kCI8-huY%TlW{zR~>|B#o zv6W|aNR#YCPpc(pcg>BYN{edi2GY%<8$Vs?YVi<{eKxyYbV>YBQpm^`p6U#z77tik za^oELWlZ1kDEeZaQ@q~OeLA~La-19|`1N;rP0rSFd$x`j{gztn=yFIEazkT1?OgQs z;uC+pmiWNd^Zs&4$j{6SODuAR*!#k7AM6zl96IhjFRzZ)ai#K>Gv8y<`ZMHg@AIAx z73CR}y9+1zIM)9yV(yYBXjjrXr^r5=irl>gq7bt6_+ ztLjVcUhVD8F=Qp}Z|fD$eDbWSks_kTEQssdt?oGcwcVo9=&~JPmrNhLhMWtcnlm~I z3N_fwM`Y!MBbYG$m(|^AJ+02vYTMe1WC>@3pbhychaaFiv2NVy#J>LI$W@hL6USwK z*4tl~)A><%^!HO+t9nf-vnCWgtF;uoIuc&m|1Z<0zh0Z*mp1WO$I+`R*F1|(sQ8XC zw_j5}@uht-x#BQ&@YasWUxZ(1$`}`NTqGxq1~pZ%gaZ$0^b9eUd|STMT0%za=) zeKG%!YyVW!iN(~Dma=)YF_WMEWy_yz{PySp8>Av$4%-MtuTau77*JyvoG4f<28#`F z1jAw16RUA80w_dlGpOY;P@IT^wQAUgWI(D@05$ckW|e3_lq3dVvPg_K58!f$lc5A} zWK7u8hzIE2$e3JsrH_!YSg<{hka;mi>v4FnX^zn2vEem3Lgwu?%9hC(?H4u@c(~uS zWij}$zm1SFz2AxhH%Qf5#9*H2BGkzU { + const draws = { + regular: "Hello World", + b: "Hello World", + boldSpan: `Hello World`, + i: "Hello World", + italicSpan: `Hello World`, + bi: "Hello World", + boldItalicSpan: `Hello World`, + bInSpan: `Hello
World`, + boldSpanInSpan: `Hello
World`, + bWithStyle: `Hello`, + spanWithStyle: `Hello`, + }; + const xml = ` + + + + + `; + const factory = new XFAFactory(new Map([["xdp:xdp", xml]])); + // Glyph widths differ per variant, so the selected font shows in the size. + const makeFont = (name, fontWeight, italicAngle, width) => ({ + name, + cssFontInfo: { fontFamily: "Helvetica", fontWeight, italicAngle }, + lineHeight: 1.2, + lineGap: 0.2, + defaultWidth: width, + encodeString: str => [str], + charsToGlyphs: str => Array.from(str, unicode => ({ unicode, width })), + }); + factory.setFonts([ + makeFont("Helvetica", 400, 0, 500), + makeFont("Helvetica-Bold", 700, 0, 600), + makeFont("Helvetica-Oblique", 400, -12, 550), + makeFont("Helvetica-BoldOblique", 700, -12, 650), + ]); + + expect(await factory.getNumPages()).toEqual(1); + + const pages = await factory.getPages(); + const container = searchHtmlNode(pages, "xfaName", "draws", true); + const sizes = new Map( + container.children.map(({ attributes, children }) => { + const { width, height } = attributes.style; + return [children[0].attributes.xfaName, [width, height]]; + }) + ); + + expect(sizes.size).toEqual(Object.keys(draws).length); + expect(sizes.get("b")).not.toEqual(sizes.get("regular")); + expect(sizes.get("i")).not.toEqual(sizes.get("regular")); + for (const [element, span] of [ + ["b", "boldSpan"], + ["i", "italicSpan"], + ["bi", "boldItalicSpan"], + ["bInSpan", "boldSpanInSpan"], + ["bWithStyle", "spanWithStyle"], + ]) { + expect(sizes.get(element)).withContext(element).toEqual(sizes.get(span)); + } + }); }); diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css index 73560fec34900..0832edb946c12 100644 --- a/web/xfa_layer_builder.css +++ b/web/xfa_layer_builder.css @@ -113,6 +113,14 @@ color: blue; } +.xfaLayer b { + font-weight: bold; +} + +.xfaLayer i { + font-style: italic; +} + .xfaRich li { margin-left: 3em; }