import { readFileSync } from "node:fs"; import { join } from "node:path"; import { PDFDocument, PDFDict, PDFName, PDFRawStream, StandardFonts, decodePDFRawStream } from "pdf-lib"; import { describe, expect, it } from "vitest"; import { collectActualTextRuns, fixPdfActualText } from "@/lib/pdf-actual-text"; // ไฟล์คำสั่งศาลปกครองจริง — cmap ของฟอนต์เพี้ยน (สระ/วรรณยุกต์กลายเป็น control char) // แต่ฝัง /ActualText ถูกไว้ใน marked content const SAMPLE = readFileSync( join(process.cwd(), "public", "note", "samples", "court_command_11_123 1.pdf"), ); describe("collectActualTextRuns — เรียนรู้ glyph จาก ActualText ของไฟล์จริง", () => { it("เจอรันหลักพัน + มีอย่างน้อย 1 ฟอนต์", async () => { const pdfDoc = await PDFDocument.load(SAMPLE, { ignoreEncryption: true }); const runs = collectActualTextRuns(pdfDoc); const total = Array.from(runs.values()).reduce((n, list) => n + list.length, 0); expect(runs.size).toBeGreaterThanOrEqual(1); expect(total).toBeGreaterThan(500); }); }); describe("fixPdfActualText — ซ่อม ToUnicode บนสำเนาในหน่วยความจำ", () => { it("ไฟล์ cmap เพี้ยน: ได้ bytes ใหม่ ที่ CMap มีสระ/วรรณยุกต์ถูกต้อง", async () => { const out = await fixPdfActualText(new Uint8Array(SAMPLE)); expect(Buffer.from(out).equals(SAMPLE)).toBe(false); // มีการแก้จริง // โหลดซ้ำ → หา ToUnicode ของทุกฟอนต์ → ต้องมี ู (U+0E39) และ ี (U+0E35) โผล่ const doc = await PDFDocument.load(out, { ignoreEncryption: true }); let cmapAll = ""; for (const page of doc.getPages()) { const fontRes = page.node.Resources()?.lookup(PDFName.of("Font")); if (!(fontRes instanceof PDFDict)) continue; for (const [, ref] of Array.from(fontRes.entries())) { const font = doc.context.lookup(ref); if (!(font instanceof PDFDict)) continue; const toUni = font.get(PDFName.of("ToUnicode")); if (!toUni) continue; const stream = doc.context.lookup(toUni); if (stream instanceof PDFRawStream) { cmapAll += Buffer.from(decodePDFRawStream(stream).decode()).toString("latin1"); } } } expect(cmapAll).toMatch(/<0E39>/); // ู expect(cmapAll).toMatch(/<0E35>/); // ี expect(cmapAll).not.toMatch(//); // ไม่ใส่ BOM ในแต่ละ entry }); it("PDF ปกติไม่มี ActualText: คืน bytes เดิมทั้งก้อน (ไม่แตะ)", async () => { const doc = await PDFDocument.create(); const font = await doc.embedFont(StandardFonts.Helvetica); doc.addPage().drawText("hello", { font, size: 12 }); const bytes = await doc.save(); const out = await fixPdfActualText(bytes); expect(Buffer.from(out).equals(Buffer.from(bytes))).toBe(true); }); });