- 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)
203 lines
6.7 KiB
Text
203 lines
6.7 KiB
Text
/**
|
|
* Lambda déclenchée par S3 ObjectCreated
|
|
* Convertit automatiquement les PDF en images JPEG avec pdfjs-dist + canvas
|
|
* et les stocke dans S3 pour les pages de signature
|
|
*/
|
|
|
|
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
const { getDocument } = require('pdfjs-dist/legacy/build/pdf.js');
|
|
const { createCanvas } = require('canvas');
|
|
const sharp = require('sharp');
|
|
|
|
// Client S3
|
|
const s3Client = new S3Client({ region: process.env.AWS_REGION || 'eu-west-3' });
|
|
|
|
const SOURCE_BUCKET = process.env.SOURCE_BUCKET || 'odentas-sign';
|
|
const DEST_BUCKET = process.env.DEST_BUCKET || 'odentas-docs';
|
|
|
|
/**
|
|
* Convertit une page PDF en image JPEG avec Sharp
|
|
*/
|
|
async function convertPdfPageToJpeg(pdfPage, scale = 2.0) {
|
|
const viewport = pdfPage.getViewport({ scale });
|
|
const canvas = createCanvas(viewport.width, viewport.height);
|
|
const context = canvas.getContext('2d');
|
|
|
|
await pdfPage.render({
|
|
canvasContext: context,
|
|
viewport: viewport,
|
|
}).promise;
|
|
|
|
// Convertir le canvas en buffer puis optimiser avec Sharp
|
|
const rawImageBuffer = canvas.toBuffer('raw');
|
|
|
|
return await sharp(rawImageBuffer, {
|
|
raw: {
|
|
width: Math.floor(viewport.width),
|
|
height: Math.floor(viewport.height),
|
|
channels: 4, // RGBA
|
|
},
|
|
})
|
|
.jpeg({
|
|
quality: 90,
|
|
progressive: true,
|
|
})
|
|
.resize(1400, null, { // Max 1400px de largeur
|
|
withoutEnlargement: true,
|
|
fit: 'inside',
|
|
})
|
|
.toBuffer();
|
|
}
|
|
|
|
/**
|
|
* Handler principal
|
|
*/
|
|
exports.handler = async (event) => {
|
|
console.log('[Lambda] Event reçu:', JSON.stringify(event, null, 2));
|
|
|
|
try {
|
|
// Récupérer les informations du fichier uploadé
|
|
const record = event.Records[0];
|
|
const bucket = record.s3.bucket.name;
|
|
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
|
|
|
|
console.log(`[Lambda] Fichier détecté: s3://${bucket}/${key}`);
|
|
|
|
// Extraire le requestId depuis la clé S3
|
|
// Format attendu: source/{folder}/{requestId}-{timestamp}.pdf
|
|
const keyParts = key.split('/');
|
|
const filename = keyParts[keyParts.length - 1];
|
|
const requestId = filename.split('-')[0]; // Prend la partie avant le premier tiret
|
|
|
|
if (!requestId) {
|
|
throw new Error(`Impossible d'extraire requestId depuis la clé: ${key}`);
|
|
}
|
|
|
|
console.log(`[Lambda] Request ID extrait: ${requestId}`);
|
|
|
|
// 1. Télécharger le PDF depuis S3
|
|
console.log('[Lambda] Téléchargement du PDF depuis S3...');
|
|
const getObjectCommand = new GetObjectCommand({
|
|
Bucket: bucket,
|
|
Key: key,
|
|
});
|
|
const s3Response = await s3Client.send(getObjectCommand);
|
|
const pdfBuffer = Buffer.from(await s3Response.Body.transformToByteArray());
|
|
console.log(`[Lambda] PDF téléchargé: ${pdfBuffer.length} bytes`);
|
|
|
|
// 2. Upload sur Cloudinary pour conversion (upload en tant que PDF, pas en JPG)
|
|
console.log('[Lambda] Upload sur Cloudinary pour conversion...');
|
|
const uploadResult = await new Promise((resolve, reject) => {
|
|
const uploadStream = cloudinary.uploader.upload_stream(
|
|
{
|
|
folder: `odentas-sign-temp/${requestId}`,
|
|
resource_type: 'raw', // 'raw' pour préserver le PDF tel quel
|
|
public_id: `pdf-${requestId}`,
|
|
transformation: [
|
|
{ width: 1400, crop: 'scale' },
|
|
{ quality: 90 },
|
|
],
|
|
},
|
|
(error, result) => {
|
|
if (error) reject(error);
|
|
else resolve(result);
|
|
}
|
|
);
|
|
|
|
bufferToStream(pdfBuffer).pipe(uploadStream);
|
|
});
|
|
|
|
console.log('[Lambda] Upload Cloudinary réussi:', uploadResult.secure_url);
|
|
console.log('[Lambda] Pages détectées par Cloudinary:', uploadResult.pages);
|
|
|
|
const baseUrl = uploadResult.secure_url;
|
|
|
|
// Cloudinary ne retourne pas toujours le nombre de pages correctement
|
|
// On va tester chaque page jusqu'à ce qu'on obtienne une erreur 404
|
|
const pages = [];
|
|
let pageNum = 1;
|
|
const maxPages = 100; // Sécurité pour éviter une boucle infinie
|
|
|
|
console.log('[Lambda] Détection du nombre de pages...');
|
|
|
|
// 3. Pour chaque page, télécharger depuis Cloudinary et stocker sur S3
|
|
while (pageNum <= maxPages) {
|
|
// Pour un PDF uploadé en 'raw', on doit forcer le format JPG avec .jpg
|
|
const public_id_with_folder = `${uploadResult.folder}/${uploadResult.public_id}`;
|
|
const cloudinaryPageUrl = `https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/pg_${pageNum}/w_1400,q_90/${public_id_with_folder}.jpg`;
|
|
|
|
console.log(`[Lambda] Test page ${pageNum}: Téléchargement depuis Cloudinary...`);
|
|
|
|
// Télécharger l'image
|
|
const imageResponse = await fetch(cloudinaryPageUrl);
|
|
|
|
// Si 4xx, on a atteint la fin du PDF (404 ou 400 selon Cloudinary)
|
|
if (imageResponse.status >= 400 && imageResponse.status < 500) {
|
|
console.log(`[Lambda] Page ${pageNum} non trouvée (${imageResponse.status}), fin du PDF`);
|
|
break;
|
|
}
|
|
|
|
if (!imageResponse.ok) {
|
|
throw new Error(`Erreur téléchargement page ${pageNum}: ${imageResponse.statusText}`);
|
|
}
|
|
|
|
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
|
|
|
|
// Stocker sur S3
|
|
const s3Key = `odentas-sign-images/${requestId}/page-${pageNum}.jpg`;
|
|
await s3Client.send(
|
|
new PutObjectCommand({
|
|
Bucket: DEST_BUCKET,
|
|
Key: s3Key,
|
|
Body: imageBuffer,
|
|
ContentType: 'image/jpeg',
|
|
CacheControl: 'public, max-age=31536000',
|
|
Metadata: {
|
|
'source-pdf': key,
|
|
'request-id': requestId,
|
|
'page-number': pageNum.toString(),
|
|
'converted-at': new Date().toISOString(),
|
|
},
|
|
})
|
|
);
|
|
|
|
pages.push(s3Key);
|
|
console.log(`[Lambda] Page ${pageNum} stockée sur S3 → ${s3Key}`);
|
|
|
|
pageNum++;
|
|
}
|
|
|
|
const totalPages = pages.length;
|
|
|
|
// 4. Nettoyer Cloudinary
|
|
console.log('[Lambda] Nettoyage Cloudinary...');
|
|
try {
|
|
await cloudinary.uploader.destroy(`odentas-sign-temp/${requestId}/pdf-${requestId}`);
|
|
console.log('[Lambda] Nettoyage Cloudinary terminé');
|
|
} catch (cleanupError) {
|
|
console.warn('[Lambda] Erreur nettoyage Cloudinary (non bloquant):', cleanupError);
|
|
}
|
|
|
|
console.log(`[Lambda] ✅ Conversion terminée: ${totalPages} page(s) pour ${requestId}`);
|
|
|
|
return {
|
|
statusCode: 200,
|
|
body: JSON.stringify({
|
|
success: true,
|
|
requestId,
|
|
totalPages,
|
|
message: 'PDF converti avec succès',
|
|
}),
|
|
};
|
|
} catch (error) {
|
|
console.error('[Lambda] ❌ Erreur:', error);
|
|
|
|
return {
|
|
statusCode: 500,
|
|
body: JSON.stringify({
|
|
success: false,
|
|
error: error.message,
|
|
}),
|
|
};
|
|
}
|
|
};
|