From 8e00cef24ee366a079a5efc8844cf6410f2c26bf Mon Sep 17 00:00:00 2001 From: mizzlelover Date: Thu, 10 Sep 2026 22:37:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=89=88=E5=A4=B4=E5=9D=90?= =?UTF-8?q?=E6=A0=87=E9=87=8F=E6=B5=8B=E7=9A=84=E4=B8=AD=E6=96=87=E5=88=86?= =?UTF-8?q?=E8=AF=8D=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/measure-header-coordinates.mjs | 45 +++++++++++++++++++++------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/tests/measure-header-coordinates.mjs b/tests/measure-header-coordinates.mjs index a92da94..b07b87d 100644 --- a/tests/measure-header-coordinates.mjs +++ b/tests/measure-header-coordinates.mjs @@ -20,16 +20,41 @@ function decodeHtml(value) { function words(pdf) { const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" }); - return [...html.matchAll(/]*yMin="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({ - yMinPt: Number(match[1]), - text: decodeHtml(match[2]), + return [...html.matchAll(/]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({ + xMinPt: Number(match[1]), + yMinPt: Number(match[2]), + xMaxPt: Number(match[3]), + text: decodeHtml(match[4]), })); } -function findWord(pdfWords, expected) { - const word = pdfWords.find(({ text }) => text.includes(expected)); - if (!word) throw new Error(`${expected} was not found on the first page`); - return word; +function findText(pdfWords, expected) { + // Poppler can split a Chinese phrase into adjacent elements depending + // on the installed font. Rebuild visual rows before matching so the + // measurement is independent of that tokenization choice. + const rows = []; + for (const word of [...pdfWords].sort((left, right) => left.yMinPt - right.yMinPt || left.xMinPt - right.xMinPt)) { + const row = rows.find(({ yMinPt }) => Math.abs(yMinPt - word.yMinPt) <= 1.5); + if (row) row.words.push(word); + else rows.push({ yMinPt: word.yMinPt, words: [word] }); + } + for (const row of rows) { + const ordered = row.words.sort((left, right) => left.xMinPt - right.xMinPt); + const text = ordered.map(({ text }) => text).join(""); + const start = text.indexOf(expected); + if (start < 0) continue; + const matchingWords = []; + let offset = 0; + for (const word of ordered) { + const end = offset + word.text.length; + if (end > start && offset < start + expected.length) matchingWords.push(word); + offset = end; + } + return { + yMinPt: Math.min(...matchingWords.map(({ yMinPt }) => yMinPt)), + }; + } + throw new Error(`${expected} was not found on the first page`); } function mm(word) { @@ -38,9 +63,9 @@ function mm(word) { const withFields = words(withFieldsPdf); const withoutFields = words(withoutFieldsPdf); -const copy = findWord(withFields, copyNo); -const agencyWithFields = findWord(withFields, agencyName); -const agencyWithoutFields = findWord(withoutFields, agencyName); +const copy = findText(withFields, copyNo); +const agencyWithFields = findText(withFields, agencyName); +const agencyWithoutFields = findText(withoutFields, agencyName); const copyMm = mm(copy); const agencyWithFieldsMm = mm(agencyWithFields); const agencyWithoutFieldsMm = mm(agencyWithoutFields);