Files
itinova-budget-si/server/payrollPdfText.ts

40 lines
1.8 KiB
TypeScript

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<string> {
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<string> {
const parsed = await pdfParse(pdf, { pagerender: renderPayrollPage });
return parsed.text;
}