Checkpoint: Fenêtre administrateur Salaires finalisée : import multipart limité, chiffrement AES-256-GCM avant stockage persistant hors conteneur, métadonnées/index métier minimisés en base, consultation PDF déchiffrée côté serveur avec no-store, filtres année/mois et comparaison M-1 expliquée. Réindexation sûre des archives partielles sans écrasement du PDF. Validation réelle sur deux liasses mensuelles : index partiel signalé sans invention de données, comparaison affichée et lecture après redémarrage. 55 tests Vitest, TypeScript et build de production validés.
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s
This commit is contained in:
39
server/payrollPdfText.ts
Normal file
39
server/payrollPdfText.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user