espace-paie-odentas/lib/odentas-sign/placeholders.ts
odentas 59749d481b feat: Migration Cloudinary vers Poppler pour conversion PDF→JPEG
- Remplacer Cloudinary (US) par solution 100% AWS eu-west-3
- Lambda odentas-sign-pdf-converter avec pdftoppm
- Lambda Layer poppler-utils v5 avec dépendances complètes
- Trigger S3 ObjectCreated pour conversion automatique
- Support multi-pages validé (PDF 3 pages)
- Stockage images dans S3 odentas-docs
- PDFImageViewer pour affichage images converties
- Conformité RGPD garantie (données EU uniquement)
2025-10-28 10:22:45 +01:00

126 lines
3.7 KiB
TypeScript

// Utilities to extract DocuSeal-style signature placeholders from PDFs
// and estimate reasonable positions when exact text coordinates are unavailable.
export type PlaceholderMatch = {
fullMatch: string;
label: string;
role: string;
type: string;
width: number; // mm
height: number; // mm
startIndex: number;
endIndex: number;
};
export type EstimatedPosition = {
role: string;
label: string;
page: number; // 1-indexed
x: number; // mm from left
y: number; // mm from top
width: number; // mm
height: number; // mm
};
const PLACEHOLDER_REGEX = /\{\{([^;]+);role=([^;]+);type=([^;]+);height=(\d+);width=(\d+)\}\}/g;
/**
* Count PDF pages by scanning for '/Type /Page' markers in raw bytes.
* This is heuristic but robust enough for most PDFs without full parsing.
*/
export function countPdfPagesFromBytes(bytes: Uint8Array | Buffer): number {
try {
const text = bufferToLatin1String(bytes);
// Count '/Type /Page' but not '/Type /Pages'
const matches = text.match(/\/Type\s*\/Page(?!s)\b/g);
if (matches && matches.length > 0) return matches.length;
} catch {
// ignore
}
// Default to 1 page if unknown
return 1;
}
/**
* Extract placeholders from PDF bytes by regex scanning the raw stream text.
*/
export function extractPlaceholdersFromPdfBuffer(bytes: Uint8Array | Buffer): PlaceholderMatch[] {
const text = bufferToLatin1String(bytes);
const placeholders: PlaceholderMatch[] = [];
PLACEHOLDER_REGEX.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
placeholders.push({
fullMatch: match[0],
label: match[1].trim(),
role: match[2].trim(),
type: match[3].trim(),
height: parseInt(match[4], 10),
width: parseInt(match[5], 10),
startIndex: match.index,
endIndex: match.index + match[0].length,
});
}
return placeholders;
}
/**
* Estimate reasonable positions (in mm) for placeholders when exact coordinates are unknown.
* Assumes A4 portrait (210 x 297mm). Places fields near the bottom margin, left/right by role.
*/
export function estimatePositionsFromPlaceholders(
placeholders: PlaceholderMatch[],
pageCount: number
): EstimatedPosition[] {
const A4_WIDTH_MM = 210;
const A4_HEIGHT_MM = 297;
const MARGIN_X_MM = 20;
const MARGIN_BOTTOM_MM = 30;
// Prefer placing on the last page by default
const defaultPage = Math.max(1, pageCount);
return placeholders.map((ph) => {
// Les placeholders portent déjà les dimensions attendues du cadre
// de signature (en millimètres), on les utilise telles quelles.
const width = Math.max(20, ph.width || 150); // mm
const height = Math.max(10, ph.height || 60); // mm
// Default Y: bottom area
const y = A4_HEIGHT_MM - MARGIN_BOTTOM_MM - height;
// Role-based horizontal placement: employer left, employee right
const roleLc = ph.role.toLowerCase();
const isEmployee = roleLc.includes('salari') || roleLc.includes('employé') || roleLc.includes('employe');
const x = isEmployee
? Math.max(MARGIN_X_MM, A4_WIDTH_MM - MARGIN_X_MM - width)
: MARGIN_X_MM;
return {
role: ph.role,
label: ph.label,
page: defaultPage,
x,
y,
width,
height,
};
});
}
function bufferToLatin1String(bytes: Uint8Array | Buffer): string {
if (typeof Buffer !== 'undefined' && (bytes as Buffer).toString) {
return (bytes as Buffer).toString('latin1');
}
// Fallback for environments without Node Buffer
let result = '';
const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) {
const slice = bytes.slice(i, i + chunk);
result += Array.from(slice as any, (b: number) => String.fromCharCode(b)).join('');
}
return result;
}