import { readFileSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; // patch fake-space ไทยของ pdf.js worker — scripts/patch-pdf-worker.mjs (bug 2026-09-24): // pdf.js เก็บตำแหน่งอ้างอิง glyph ก่อนหน้า (prevTransform) เฉพาะ glyph ที่กว้าง ≠ 0 // ทำให้ charSpacing ของสระบน-ล่าง/วรรณยุกต์ (glyph กว้าง 0) สะสมเป็น gap จน // addFakeSpaces แทรก " " กลางคำ ("ฉะนั้ น") — patch ถอด if ครอบการอัปเดตออก // ให้อัปเดตทุก glyph แทน const ROOT = process.cwd(); // statics ของแอปอยู่ใต้ public/note/** (URL /docedit/note/… — basePath /docedit + prefix /note) const WORKER = join(ROOT, "public", "note", "pdf.worker.min.mjs"); // ไฟล์ Word→PDF ที่ justify ด้วย letter-spacing ทุก glyph (มี charSpacing ~0.05×fontSize) // — เงื่อนไขที่ทำให้ heuristic ของ pdf.js แทรก fake space กลางคำไทย // (อยู่ใน tests/fixtures/ ไม่ใช่ public/note/samples/ — ไม่ให้ seed-samples สร้าง // รายการเอกสาร "ทดสอบ" บนหน้ารายการของทุก deploy) const SAMPLE = join(ROOT, "tests", "fixtures", "thai_letterspacing.pdf"); describe("public/note/pdf.worker.min.mjs — ผ่าน patch fake-space ไทย", () => { const code = readFileSync(WORKER, "utf8"); it("อัปเดต prevTransform ทุก glyph (รูปแบบก่อนแพตช์ C&&(h.prevTransform=…) หายไป)", () => { // หลัง patch: }h.prevTransform=getCurrentTextTransform(); (เคยถูก if ครอบ // จนเหลือ } ปิด block เปล่านำหน้า) expect(code).toMatch( /[;{}][A-Za-z$_][\w$]*\.prevTransform=getCurrentTextTransform\(\);/, ); // ก่อน patch: C&&(h.prevTransform=getCurrentTextTransform()); expect(code).not.toMatch( /&&\([A-Za-z$_][\w$]*\.prevTransform=getCurrentTextTransform\(\)\);/, ); }); }); describe("getTextContent ผ่าน worker ที่ patch แล้ว — ไฟล์ justify letter-spacing", () => { it("ไม่มี fake space กลางคำไทย (space ตามด้วยสระ/วรรณยุกต์ = ผิดเสมอ)", async () => { const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); pdfjs.GlobalWorkerOptions.workerSrc = pathToFileURL(WORKER).href; const doc = await pdfjs.getDocument({ data: new Uint8Array(readFileSync(SAMPLE)), }).promise; // space หน้า combining mark ไม่มีทางถูกต้องในภาษาไทย (mark ต้องเกาะฐานหน้า) const badRe = / [ัิีึืุู็่้๊๋์ํ]/; const offenders: string[] = []; let targetLine = ""; for (let p = 1; p <= doc.numPages; p++) { const tc = await (await doc.getPage(p)).getTextContent(); for (const item of tc.items) { if (!("str" in item)) continue; if (!targetLine && item.str.includes("ฉะนั้น")) targetLine = item.str; if (badRe.test(item.str)) { offenders.push(`p${p}: ${JSON.stringify(item.str.slice(0, 60))}`); } } } expect(offenders).toEqual([]); // บรรทัดเป้าหมาย (เคยได้ "ฉะนั้ น ให้ ท่ าน…คำฟ้ อง") — space จริง (glyph SPACE // ในไฟล์) คงอยู้ครบ ส่วนช่อง letter-spacing เล็ก ๆ ที่ไม่ใช่ space จริงติดกัน // ("ให้การแก้" — ในไฟล์ไม่มี space glyph ตรงนั้น) expect(targetLine).toBe( "ฉะนั้น ให้ ท่านทำคำให้การแก้คำฟ้อง พร้อมด้วยพยานหลักฐาน ๑ ชุด", ); }); });