- Remplacement de DocuSeal par solution souveraine Odentas Sign - Système d'authentification OTP pour signataires (bcryptjs + JWT) - 8 routes API: send-otp, verify-otp, sign, pdf-url, positions, status, webhook, signers - Interface moderne avec canvas de signature et animations (framer-motion, confetti) - Système de templates pour auto-détection des positions de signature (CDDU, RG, avenants) - PDF viewer avec @react-pdf-viewer (compatible Next.js) - Stockage S3: source/, signatures/, evidence/, signed/, certs/ - Tables Supabase: sign_requests, signers, sign_positions, sign_events, sign_assets - Evidence bundle automatique (JSON metadata + timestamps) - Templates emails: OTP et completion - Scripts Lambda prêts: pades-sign (KMS seal) et tsaStamp (RFC3161) - Mode test détecté automatiquement (emails whitelist) - Tests complets avec PDF CDDU réel (2 signataires)
308 lines
10 KiB
JavaScript
Executable file
308 lines
10 KiB
JavaScript
Executable file
#!/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 <chemin-pdf> [--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);
|
|
});
|