From 6cbce160f33e78db1741a2df32c833507b5f5b70 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 3 Jul 2025 18:01:33 +0200 Subject: [PATCH] Fix pdf generation and parsing --- src/front/Services/PdfService/index.ts | 478 +++---------------------- 1 file changed, 42 insertions(+), 436 deletions(-) diff --git a/src/front/Services/PdfService/index.ts b/src/front/Services/PdfService/index.ts index 1b2e7af5..24533913 100644 --- a/src/front/Services/PdfService/index.ts +++ b/src/front/Services/PdfService/index.ts @@ -1,6 +1,8 @@ import { saveAs } from 'file-saver'; import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; +const separator = '\x1f'; + export interface CustomerInfo { firstName: string; lastName: string; @@ -185,50 +187,17 @@ export default class PdfService { }); y -= lineSpacing; - // Document Hash Section - page.drawText('Intégrité du Document', { - x: leftMargin, - y: y, - size: 12, - font: helveticaBoldFont, - color: rgb(0, 0, 0) - }); - y -= lineSpacing; - - // Add verification data as base64 - const verificationData = { - documentHash: certificateData.documentHash, - commitmentId: certificateData.metadata.commitmentId, - merkleProof: certificateData.metadata.merkleProof || "N/A" - }; - const verificationDataBase64 = btoa(JSON.stringify(verificationData)); - - // Calculate proper chunk size based on available width and font size - const availableWidth = width - (2 * leftMargin) - 20; // Full width minus margins and safety buffer - const fontSize = 12; - // Use a typical base64 character for width calculation - const avgCharWidth = helveticaFont.widthOfTextAtSize('A', fontSize); - const charsPerLine = Math.floor(availableWidth / avgCharWidth); - const chunkSize = Math.max(60, Math.min(100, charsPerLine)); // More conservative limits - - const chunks = []; - for (let j = 0; j < verificationDataBase64.length; j += chunkSize) { - chunks.push(verificationDataBase64.slice(j, j + chunkSize)); - } - chunks.forEach((chunk, index) => { - page.drawText(chunk, { - x: leftMargin, - y: y - (index * 12), // Increased line spacing - size: fontSize, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - }); + const keywords = [ + certificateData.documentHash, + certificateData.metadata.commitmentId, + JSON.stringify(certificateData.metadata.merkleProof) + ]; + pdfDoc.setKeywords([keywords.join(separator)]); // Add explanatory text about certificate usage - y -= (chunks.length * 12) + 40; // Space after verification data const explanatoryText = "Ce certificat doit être utilisé avec le document qu'il certifie pour être vérifié sur la page dédiée. Le document correspondant à ce certificat doit être téléchargé depuis LeCoffre et peut être conservé avec le certificat tant qu'il n'est pas modifié."; const explanatoryLines = this.splitTextToFit(helveticaObliqueFont, explanatoryText, width - (2 * leftMargin), 11); + y -= (explanatoryLines.length * 12) + 40; // Space after verification data explanatoryLines.forEach((line, index) => { page.drawText(line, { x: leftMargin, @@ -322,314 +291,47 @@ export default class PdfService { } } - /** - * Generate certificate for notary documents - * @param certificateData - Data needed for the certificate - * @returns Promise - The generated PDF as a blob - */ - public async generateNotaryCertificate(certificateData: CertificateData): Promise { - try { - // Create a new PDF document - const pdfDoc = await PDFDocument.create(); - const page = pdfDoc.addPage([595, 842]); // A4 size - const { width, height } = page.getSize(); - - // Embed the standard font - const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); - const helveticaBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); - const helveticaObliqueFont = await pdfDoc.embedFont(StandardFonts.HelveticaOblique); - - // Notary Information Section (Top Left) - page.drawText('Notaire Validateur:', { - x: 20, - y: height - 30, - size: 12, - font: helveticaBoldFont, - color: rgb(0, 0, 0) - }); - - page.drawText(certificateData.notary.name, { - x: 20, - y: height - 37, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - - // Customer Information Section (Top Right) - page.drawText('Fournisseur de Document:', { - x: 120, - y: height - 30, - size: 12, - font: helveticaBoldFont, - color: rgb(0, 0, 0) - }); - - page.drawText(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, { - x: 120, - y: height - 37, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - - page.drawText(certificateData.customer.email, { - x: 120, - y: height - 44, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - - page.drawText(certificateData.customer.postalAddress, { - x: 120, - y: height - 51, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - - // Centered Title - const titleText = 'Certificat Notarial de Validation de Document'; - const titleWidth = helveticaBoldFont.widthOfTextAtSize(titleText, 20); - page.drawText(titleText, { - x: (width - titleWidth) / 2, - y: height - 80, - size: 20, - font: helveticaBoldFont, - color: rgb(0, 0, 0) - }); - - // Add a line separator - page.drawLine({ - start: { x: 20, y: height - 90 }, - end: { x: width - 20, y: height - 90 }, - thickness: 0.5, - color: rgb(0, 0, 0) - }); - - let yPosition = height - 110; - - // Document Information Section - page.drawText('Informations du Document', { - x: 20, - y: yPosition, - size: 12, - font: helveticaBoldFont, - color: rgb(0, 0, 0) - }); - yPosition -= 10; - - page.drawText(`Identifiant du Document: ${certificateData.metadata.documentUid}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`Type de Document: ${certificateData.metadata.documentType}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`Numéro de dossier: ${certificateData.folderUid}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`Nom du fichier: ${certificateData.metadata.fileName}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`ID de transaction: ${certificateData.metadata.commitmentId}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - page.drawText(`Statut: ${certificateData.metadata.isDeleted ? 'Supprimé' : 'Actif'}`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 15; - - // Document Hash Section - page.drawText('Intégrité du Document', { - x: 20, - y: yPosition, - size: 12, - font: helveticaBoldFont, - color: rgb(0, 0, 0) - }); - yPosition -= 10; - - page.drawText(`Hash SHA256 du Document:`, { - x: 20, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - - // Split hash into multiple lines if needed - const hash = certificateData.documentHash; - const maxWidth = 170; // Available width for text - const hashLines = this.splitTextToFit(helveticaFont, hash, maxWidth, 12); - - for (const line of hashLines) { - page.drawText(line, { - x: 25, - y: yPosition, - size: 12, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - yPosition -= 7; - } - yPosition -= 8; // Extra space after hash - - yPosition -= 5; - - // Footer - const footerText1 = `Page 1 sur 1`; - const footerText2 = `Certificat Notarial - Généré le ${new Date().toLocaleString('fr-FR')}`; - const footerWidth1 = helveticaObliqueFont.widthOfTextAtSize(footerText1, 10); - const footerWidth2 = helveticaObliqueFont.widthOfTextAtSize(footerText2, 10); - - page.drawText(footerText1, { - x: (width - footerWidth1) / 2, - y: 30, - size: 10, - font: helveticaObliqueFont, - color: rgb(0, 0, 0) - }); - - page.drawText(footerText2, { - x: (width - footerWidth2) / 2, - y: 25, - size: 10, - font: helveticaObliqueFont, - color: rgb(0, 0, 0) - }); - - // Add verification data as base64 in small font - const verificationData = { - documentHash: certificateData.documentHash, - commitmentId: certificateData.metadata.commitmentId, - merkleProof: certificateData.metadata.merkleProof || "N/A" - }; - const verificationDataBase64 = btoa(JSON.stringify(verificationData)); - - // Split into chunks if too long - const chunkSize = 80; - const chunks = []; - for (let j = 0; j < verificationDataBase64.length; j += chunkSize) { - chunks.push(verificationDataBase64.slice(j, j + chunkSize)); - } - - chunks.forEach((chunk, index) => { - page.drawText(chunk, { - x: 20, - y: 15 - (index * 3), - size: 6, - font: helveticaFont, - color: rgb(0, 0, 0) - }); - }); - - // Save the PDF - const pdfBytes = await pdfDoc.save(); - return new Blob([pdfBytes], { type: 'application/pdf' }); - } catch (error) { - console.error('Error generating notary certificate:', error); - throw new Error('Failed to generate notary certificate'); - } - } - - /** - * Download a notary certificate PDF - * @param certificateData - Data needed for the certificate - * @param filename - Optional filename for the download - */ - public async downloadNotaryCertificate(certificateData: CertificateData, filename?: string): Promise { - try { - const pdfBlob = await this.generateNotaryCertificate(certificateData); - const defaultFilename = `notary_certificate_${certificateData.metadata.documentUid}_${new Date().toISOString().split('T')[0]}.pdf`; - saveAs(pdfBlob, filename || defaultFilename); - } catch (error) { - console.error('Error downloading notary certificate:', error); - throw error; - } - } - /** * Parse a PDF certificate and extract document information * @param pdfBlob - The PDF file as a blob * @returns Promise<{documentHash: string, documentUid: string, commitmentId: string, merkleProof?: string}> - Extracted information */ - public async parseCertificate(pdfBlob: Blob): Promise<{documentHash: string, documentUid: string, commitmentId: string, merkleProof?: string}> { + public async parseCertificate(certificateFile: File): Promise<{documentHash: string, commitmentId: string, merkleProof: any}> { return new Promise((resolve, reject) => { const fileReader = new FileReader(); + + console.log("certificateFile", certificateFile); fileReader.onload = async () => { try { - // Convert PDF to text using browser's PDF capabilities - const arrayBuffer = fileReader.result as ArrayBuffer; - const pdfData = new Uint8Array(arrayBuffer); - console.log("PDF data:", pdfData); - - // Use a simple text extraction approach - // This is a basic implementation - for production, consider using pdfjs-dist - const text = await this.extractTextFromPdf(pdfData); - - // Parse the extracted text to find our certificate data - const parsedData = this.parseCertificateText(text); - - if (!parsedData.documentHash) { - throw new Error('Document hash not found in certificate'); + // Read the metadata and get the validation data from the keywords + const pdfDoc = await PDFDocument.load(await certificateFile.arrayBuffer()); + const keywords = pdfDoc.getKeywords()?.split(separator); + + if (!keywords) { + throw new Error("No keywords found in certificate"); } - - resolve(parsedData); + + console.log(keywords); + + if (keywords.length !== 3) { + throw new Error("Invalid keywords found in certificate"); + } + + const documentHash = keywords[0]; + const commitmentId = keywords[1]; + const merkleProof = keywords[2]; + + if (!documentHash || !commitmentId || !merkleProof) { + throw new Error("Invalid keywords found in certificate"); + } + + console.log("documentHash", documentHash); + console.log("commitmentId", commitmentId); + console.log("merkleProof", merkleProof); + + + resolve({ documentHash, commitmentId, merkleProof }); } catch (error) { reject(error); } @@ -637,105 +339,9 @@ export default class PdfService { fileReader.onerror = () => { reject(new Error('Failed to read PDF file')); - }; - - fileReader.readAsArrayBuffer(pdfBlob); + }; + + fileReader.readAsArrayBuffer(certificateFile); }); } - - /** - * Extract text from PDF using browser capabilities - * @param pdfData - PDF data as Uint8Array - * @returns Promise - Extracted text - */ - private async extractTextFromPdf(pdfData: Uint8Array): Promise { - console.log("extractTextFromPdf"); - - // Convert PDF data to string to search for base64 patterns - const pdfString = new TextDecoder('utf-8').decode(pdfData); - - // Look for base64 patterns in the PDF content - // This is a simple approach that should work for our embedded data - const base64Matches = pdfString.match(/([A-Za-z0-9+/]{80,}={0,2})/g); - - if (base64Matches && base64Matches.length > 0) { - console.log('Found base64 patterns in PDF:', base64Matches.length); - // Return the longest base64 string (most likely our verification data) - const longestBase64 = base64Matches.reduce((a, b) => a.length > b.length ? a : b); - return longestBase64; - } - - // If no base64 found, return empty string - console.log('No base64 patterns found in PDF'); - return ''; - } - - /** - * Parse certificate text to extract specific fields - * @param text - Extracted text from PDF (could be base64 or regular text) - * @returns Parsed certificate data - */ - private parseCertificateText(text: string): {documentHash: string, documentUid: string, commitmentId: string, merkleProof?: string} { - const result = { - documentHash: '', - documentUid: '', - commitmentId: '', - merkleProof: undefined as string | undefined - }; - - // Check if the text is a base64 string (our new format) - if (text.match(/^[A-Za-z0-9+/]+={0,2}$/)) { - try { - console.log('Attempting to decode base64 data:', text.substring(0, 50) + '...'); - const decodedData = atob(text); - const verificationData = JSON.parse(decodedData); - - console.log('Successfully decoded verification data:', verificationData); - - if (verificationData.documentHash) { - result.documentHash = verificationData.documentHash; - } - if (verificationData.commitmentId) { - result.commitmentId = verificationData.commitmentId; - } - if (verificationData.merkleProof && verificationData.merkleProof !== "N/A") { - result.merkleProof = verificationData.merkleProof; - } - - // If we successfully extracted data from base64, return early - if (result.documentHash) { - return result; - } - } catch (error) { - console.log('Failed to decode base64 data:', error); - } - } - - // Fallback to text parsing for older certificates - // Extract document hash (64 character hex string) - const hashMatch = text.match(/Hash SHA256 du Document:\s*([a-fA-F0-9]{64})/); - if (hashMatch && hashMatch[1]) { - result.documentHash = hashMatch[1]; - } - - // Extract document UID - const uidMatch = text.match(/UID du Document:\s*([^\n\r]+)/); - if (uidMatch && uidMatch[1]) { - result.documentUid = uidMatch[1].trim(); - } - - // Extract commitment ID - const commitmentMatch = text.match(/ID de transaction:\s*([^\n\r]+)/); - if (commitmentMatch && commitmentMatch[1]) { - result.commitmentId = commitmentMatch[1].trim(); - } - - // Extract merkle proof - const merkleProofMatch = text.match(/Preuve Merkle \(Blockchain\):\s*([^\n\r]+)/); - if (merkleProofMatch && merkleProofMatch[1]) { - result.merkleProof = merkleProofMatch[1].trim(); - } - - return result; - } }