From ae7bf878d8ad4b2cbba4c7145151fff8bfa395a8 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Wed, 2 Sep 2026 21:59:31 +0200 Subject: [PATCH] fix: the parity harness reads the case a style paints, not the node's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A style's fo:text-transform / w:caps is a CSS transform in the browser, so Range.getClientRects' text stayed lower case where the reference PDF has real uppercase — three headings of one fixture reported a lineBreak that was not one, and comparison stopped at each. The computed textTransform is applied to the extracted word now. Reports rise where they were hidden behind such a line. --- tests/render-parity/run.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/render-parity/run.mjs b/tests/render-parity/run.mjs index ebff870..aa4081e 100644 --- a/tests/render-parity/run.mjs +++ b/tests/render-parity/run.mjs @@ -141,6 +141,16 @@ function extractLayout() { return out.map((f) => ({ top: f.top, left: f.left, width: f.right - f.left, text: f.text })); }; + // What the page paints, not what the node holds: a style's fo:text-transform / + // w:caps is a CSS transform here and real uppercase in the reference's PDF. + const painted = (text, el) => { + const t = getComputedStyle(el).textTransform; + if (t === 'uppercase') return text.toUpperCase(); + if (t === 'lowercase') return text.toLowerCase(); + if (t === 'capitalize') return text.replace(/^\p{L}/u, (c) => c.toUpperCase()); + return text; + }; + const words = []; const walker = document.createTreeWalker(paper, NodeFilter.SHOW_TEXT); for (let n = walker.nextNode(); n; n = walker.nextNode()) { @@ -152,7 +162,7 @@ function extractLayout() { const y = f.top - origin.top; const page = Math.floor(y / cycle); words.push({ - text: f.text, + text: painted(f.text, n.parentElement), page, x: (f.left - origin.left) * PX_MM, y: (y - page * cycle) * PX_MM,