espace-paie-odentas/lambda-pdf-converter/index.js.cloudinary.bak
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

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,
}),
};
}
};