import pdfParse from "pdf-parse/lib/pdf-parse.js"; type PdfTextItem = { str?: string; transform?: number[] }; type PdfPageData = { getTextContent: (options: { normalizeWhitespace: boolean; disableCombineTextItems: boolean }) => Promise<{ items: PdfTextItem[] }> }; /** * Reconstruit les lignes en ordonnant les fragments de texte par coordonnées. * Cette étape évite que les valeurs de colonnes distinctes (adresse, IBAN, etc.) * soient concaténées dans l'intitulé de poste par l'extracteur PDF standard. */ export async function renderPayrollPage(pageData: unknown): Promise { const pdfPage = pageData as PdfPageData; const textContent = await pdfPage.getTextContent({ normalizeWhitespace: false, disableCombineTextItems: false }); const fragments = textContent.items .filter((item) => item.str && item.transform && item.transform.length >= 6) .map((item) => ({ text: item.str!.trim(), x: item.transform![4]!, y: item.transform![5]! })) .filter((item) => item.text.length > 0) .sort((a, b) => Math.abs(b.y - a.y) > 0.75 ? b.y - a.y : a.x - b.x); const lines: Array<{ y: number; items: Array<{ text: string; x: number }> }> = []; fragments.forEach((fragment) => { const previous = lines[lines.length - 1]; if (previous && Math.abs(previous.y - fragment.y) <= 0.75) { previous.items.push({ text: fragment.text, x: fragment.x }); } else { lines.push({ y: fragment.y, items: [{ text: fragment.text, x: fragment.x }] }); } }); return `${lines .map((line) => line.items.sort((a, b) => a.x - b.x).map((item) => item.text).join(" ")) .join("\n")}\f`; } /** Extrait le texte d'une liasse en préservant les limites de pages. */ export async function extractPayrollPdfText(pdf: Buffer): Promise { const parsed = await pdfParse(pdf, { pagerender: renderPayrollPage }); return parsed.text; }