#!/usr/bin/env node /** * Outil pour extraire les positions exactes des placeholders DocuSeal * Format: {{Label;role=Role;type=signature;height=H;width=W}} * * Utilise pdf-lib pour obtenir les vraies coordonnées */ const fs = require('fs'); const path = require('path'); const { PDFDocument } = require('pdf-lib'); /** * Extrait les placeholders du texte */ function extractPlaceholdersFromText(text) { const regex = /\{\{([^;]+);role=([^;]+);type=([^;]+);height=(\d+);width=(\d+)\}\}/g; const placeholders = []; let match; while ((match = 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]), width: parseInt(match[5]), startIndex: match.index, endIndex: match.index + match[0].length, }); } return placeholders; } /** * Lit le contenu textuel d'un PDF avec pdf-lib * Note: pdf-lib ne fournit pas directement les positions du texte * On va donc utiliser une approche hybride */ async function analyzePdfWithLib(pdfPath) { const pdfBytes = fs.readFileSync(pdfPath); const pdfDoc = await PDFDocument.load(pdfBytes); const pages = pdfDoc.getPages(); const results = { pageCount: pages.length, pages: [], }; for (let i = 0; i < pages.length; i++) { const page = pages[i]; const { width, height } = page.getSize(); results.pages.push({ number: i + 1, width, height, }); } return results; } /** * Extrait le texte brut avec pdf-parse */ async function extractTextWithPdfParse(pdfPath) { try { const pdfParse = require('pdf-parse'); const dataBuffer = fs.readFileSync(pdfPath); const data = await pdfParse(dataBuffer); return data.text; } catch (error) { console.warn('⚠️ pdf-parse non disponible, utilisation de méthode alternative'); return null; } } /** * Méthode alternative : lire le PDF comme texte brut et chercher les patterns */ function extractTextFromPdfRaw(pdfPath) { const pdfBytes = fs.readFileSync(pdfPath); const pdfText = pdfBytes.toString('utf-8'); return pdfText; } /** * Estime la position Y basée sur la fréquence du texte dans le document * Méthode heuristique pour les PDFs sans extraction de coordonnées */ function estimatePositions(placeholders, pdfInfo, totalText) { const estimatedPositions = []; for (const placeholder of placeholders) { // Chercher le contexte autour du placeholder const contextBefore = totalText.substring(Math.max(0, placeholder.startIndex - 200), placeholder.startIndex); const contextAfter = totalText.substring(placeholder.endIndex, Math.min(totalText.length, placeholder.endIndex + 200)); // Estimer la page (simplifié: diviser le document en segments) const relativePosition = placeholder.startIndex / totalText.length; const estimatedPage = Math.ceil(relativePosition * pdfInfo.pageCount); // Pour un document A4 standard (842 points de hauteur) const pageHeight = pdfInfo.pages[estimatedPage - 1]?.height || 842; const pageWidth = pdfInfo.pages[estimatedPage - 1]?.width || 595; // Position Y: du haut vers le bas // Les placeholders de signature sont souvent en bas de page const estimatedY = pageHeight * 0.2; // 20% depuis le haut (donc vers le bas) // Position X: selon le rôle let estimatedX = 50; // Marge gauche par défaut if (placeholder.role.toLowerCase().includes('salarié') || placeholder.role.toLowerCase().includes('salarie') || placeholder.role.toLowerCase().includes('employé')) { estimatedX = pageWidth / 2 + 50; // Droite de la page } estimatedPositions.push({ role: placeholder.role, label: placeholder.label, page: estimatedPage, x: Math.round(estimatedX), y: Math.round(estimatedY), width: placeholder.width, height: placeholder.height, confidence: 'estimated', // Indiquer que c'est une estimation context: { before: contextBefore.substring(contextBefore.length - 50), after: contextAfter.substring(0, 50), }, }); } return estimatedPositions; } /** * Crée un mapping de positions par template */ function createTemplateMapping(positions, filename) { const templateName = filename.replace(/[_-]\w+\.pdf$/, ''); // Enlever les ID uniques return { templateName, filename, positions: positions.map(p => ({ role: p.role, page: p.page, x: p.x, y: p.y, width: p.width, height: p.height, })), metadata: { generatedAt: new Date().toISOString(), method: 'docuseal-placeholder-extraction', }, }; } async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.error('Usage: node extract-signature-positions.js [--save-template]'); console.error(''); console.error('Options:'); console.error(' --save-template Sauvegarder comme template réutilisable'); console.error(''); console.error('Exemples:'); console.error(' node extract-signature-positions.js contrat_cddu.pdf'); console.error(' node extract-signature-positions.js contrat_cddu.pdf --save-template'); process.exit(1); } const pdfPath = args[0]; const saveTemplate = args.includes('--save-template'); if (!fs.existsSync(pdfPath)) { console.error(`❌ Fichier introuvable: ${pdfPath}`); process.exit(1); } console.log('═══════════════════════════════════════════════════════'); console.log(' 🔍 Extraction des positions DocuSeal'); console.log('═══════════════════════════════════════════════════════\n'); const filename = path.basename(pdfPath); console.log(`📄 PDF: ${filename}\n`); // 1. Analyser le PDF avec pdf-lib console.log('📖 Analyse du PDF...'); const pdfInfo = await analyzePdfWithLib(pdfPath); console.log(` Pages: ${pdfInfo.pageCount}`); pdfInfo.pages.forEach(p => { console.log(` Page ${p.number}: ${Math.round(p.width)}x${Math.round(p.height)} points`); }); console.log(''); // 2. Extraire le texte console.log('📝 Extraction du texte...'); let text = await extractTextWithPdfParse(pdfPath); if (!text) { text = extractTextFromPdfRaw(pdfPath); } console.log(` ${text.length} caractères extraits\n`); // 3. Trouver les placeholders console.log('🔍 Recherche des placeholders DocuSeal...'); const placeholders = extractPlaceholdersFromText(text); if (placeholders.length === 0) { console.log(' ❌ Aucun placeholder DocuSeal trouvé'); console.log(''); console.log(' Format attendu: {{Label;role=Role;type=signature;height=H;width=W}}'); console.log(' Exemple: {{Signature Employé;role=Salarié;type=signature;height=60;width=150}}'); process.exit(1); } console.log(` ✅ ${placeholders.length} placeholder(s) trouvé(s):\n`); placeholders.forEach((p, i) => { console.log(` ${i + 1}. ${p.label}`); console.log(` Rôle: ${p.role}`); console.log(` Type: ${p.type}`); console.log(` Dimensions: ${p.width}x${p.height}px`); console.log(''); }); // 4. Estimer les positions console.log('📍 Calcul des positions...'); const positions = estimatePositions(placeholders, pdfInfo, text); console.log(''); console.log('═══════════════════════════════════════════════════════'); console.log(' 📊 Positions extraites'); console.log('═══════════════════════════════════════════════════════\n'); positions.forEach((pos, i) => { console.log(`${i + 1}. ${pos.role} - "${pos.label}"`); console.log(` Page: ${pos.page}`); console.log(` Position: (${pos.x}, ${pos.y})`); console.log(` Dimensions: ${pos.width}x${pos.height}px`); console.log(` Confiance: ${pos.confidence}`); console.log(` Contexte avant: ...${pos.context.before}`); console.log(` Contexte après: ${pos.context.after}...`); console.log(''); }); // 5. Format pour l'API console.log('═══════════════════════════════════════════════════════'); console.log(' 💻 Format pour create-real-signature.js'); console.log('═══════════════════════════════════════════════════════\n'); const positionsObject = {}; positions.forEach(pos => { positionsObject[pos.role] = { page: pos.page, x: pos.x, y: pos.y, width: pos.width, height: pos.height, }; }); console.log('```javascript'); console.log('const positions = ' + JSON.stringify(positionsObject, null, 2) + ';'); console.log('```\n'); // 6. Sauvegarder comme template si demandé if (saveTemplate) { const template = createTemplateMapping(positions, filename); const templateDir = path.join(__dirname, 'signature-templates'); if (!fs.existsSync(templateDir)) { fs.mkdirSync(templateDir); } const templateFile = path.join(templateDir, `${template.templateName}.json`); fs.writeFileSync(templateFile, JSON.stringify(template, null, 2)); console.log(`💾 Template sauvegardé: ${templateFile}\n`); } // 7. Avertissement sur les estimations console.log('⚠️ IMPORTANT:'); console.log(' Les positions sont ESTIMÉES car pdf-lib ne peut pas extraire'); console.log(' les coordonnées exactes du texte.'); console.log(''); console.log(' Pour des positions PRÉCISES, utilisez une de ces méthodes:'); console.log(' 1. Créer un template manuel basé sur vos vrais documents'); console.log(' 2. Utiliser pdf.js (plus complexe mais précis)'); console.log(' 3. Ajuster manuellement les coordonnées après tests'); console.log(''); console.log('💡 Conseil: Testez avec create-real-signature.js et ajustez'); console.log(' les positions si nécessaire.\n'); } main().catch(error => { console.error('❌ Erreur:', error.message); process.exit(1); });