From 0267892cdca66866ad5bef75adb497041706157f Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 23:26:37 +0200 Subject: [PATCH] Refactor certificate generation --- .../ClientView/DocumentTables/index.tsx | 58 +- src/front/Services/PdfService/index.ts | 717 ++++++++++++++---- 2 files changed, 599 insertions(+), 176 deletions(-) diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx index 7b9819e2..481238a1 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx @@ -210,40 +210,34 @@ export default function DocumentTables(props: IProps) { } }; - // Fetch folder data to get office/notary information - // const folderProcess = await FolderService.getFolderByUid(folderUid, false, true); - // const folderData = folderProcess?.processData; + // Get the specific document that was clicked + const documentProcess = await DocumentService.getDocumentByUid(doc.uid); + if (!documentProcess) { + throw new Error('Document not found'); + } - // console.log('[DocumentTables] onDownloadCertificate: folderData', folderData); + // Process only the files for this specific document + for (const file of documentProcess.processData.files) { + const fileProcess = await FileService.getFileByUid(file.uid); + console.log('[DocumentTables] onDownloadCertificate: fileProcess', fileProcess); + + const hash = fileProcess.lastUpdatedFileState.pcd_commitment.file_blob; + certificateData.documentHash = hash; - const documentProcesses = await DocumentService.getDocuments(); - documentProcesses.filter((process: any) => process.processData.folder.uid === folderUid); - - // console.log('[DocumentTables] onDownloadCertificate: documentProcesses', documentProcesses); - - for (const document of documentProcesses) { - for (const file of document.processData.files) { - await FileService.getFileByUid(file.uid).then((res: any) => { - console.log('[DocumentTables] onDownloadCertificate: fileProcess', res); - const hash = res.lastUpdatedFileState.pcd_commitment.file_blob; - certificateData.documentHash = hash; - - MessageBus.getInstance().generateMerkleProof(res.lastUpdatedFileState, 'file_blob').then((proof) => { - console.log('[DocumentTables] onDownloadCertificate: proof', proof); - const metadata: Metadata = { - fileName: res.processData.file_name, - isDeleted: false, - updatedAt: new Date(res.processData.updated_at), - commitmentId: res.lastUpdatedFileState.commited_in, - createdAt: new Date(res.processData.created_at), - documentUid: doc.document_type.uid, - documentType: doc.document_type.name, - merkleProof: proof - }; - certificateData.metadata = metadata; - }); - } - )} + const proof = await MessageBus.getInstance().generateMerkleProof(fileProcess.lastUpdatedFileState, 'file_blob'); + console.log('[DocumentTables] onDownloadCertificate: proof', proof); + + const metadata: Metadata = { + fileName: fileProcess.processData.file_name, + isDeleted: false, + updatedAt: new Date(fileProcess.processData.updated_at), + commitmentId: fileProcess.lastUpdatedFileState.commited_in, + createdAt: new Date(fileProcess.processData.created_at), + documentUid: doc.uid, + documentType: doc.document_type.name, + merkleProof: proof + }; + certificateData.metadata = metadata; } console.log('[DocumentTables] onDownloadCertificate: certificateData', certificateData); diff --git a/src/front/Services/PdfService/index.ts b/src/front/Services/PdfService/index.ts index 62ea9765..1b2e7af5 100644 --- a/src/front/Services/PdfService/index.ts +++ b/src/front/Services/PdfService/index.ts @@ -1,5 +1,5 @@ import { saveAs } from 'file-saver'; -import jsPDF from 'jspdf'; +import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; export interface CustomerInfo { firstName: string; @@ -49,92 +49,218 @@ export default class PdfService { */ public async generateCertificate(certificateData: CertificateData): Promise { try { - const doc = new jsPDF(); - + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage([595, 842]); // A4 size + const { width, height } = page.getSize(); + + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + const helveticaBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + const helveticaObliqueFont = await pdfDoc.embedFont(StandardFonts.HelveticaOblique); + + let y = height - 40; // Start 40pt from the top + const leftMargin = 20; + const rightSectionX = 320; + const lineSpacing = 16; + // Notary Information Section (Top Left) - doc.setFontSize(12); - doc.setFont('helvetica', 'bold'); - doc.text('Notaire Validateur:', 20, 30); - doc.setFont('helvetica', 'normal'); - doc.text(certificateData.notary.name, 20, 37); - + page.drawText('Notaire Validateur:', { + x: leftMargin, + y: y, + size: 12, + font: helveticaBoldFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + page.drawText(certificateData.notary.name, { + x: leftMargin, + y: y, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + // Customer Information Section (Top Right) - doc.setFont('helvetica', 'bold'); - doc.text('Fournisseur de Document:', 120, 30); - doc.setFont('helvetica', 'normal'); - doc.text(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, 120, 37); - doc.text(certificateData.customer.email, 120, 44); - doc.text(certificateData.customer.postalAddress, 120, 51); - + let yRight = height - 40; + page.drawText('Fournisseur de Document:', { + x: rightSectionX, + y: yRight, + size: 12, + font: helveticaBoldFont, + color: rgb(0, 0, 0) + }); + yRight -= lineSpacing; + page.drawText(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, { + x: rightSectionX, + y: yRight, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + yRight -= lineSpacing; + page.drawText(certificateData.customer.email, { + x: rightSectionX, + y: yRight, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + yRight -= lineSpacing; + page.drawText(certificateData.customer.postalAddress, { + x: rightSectionX, + y: yRight, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + // Centered Title - doc.setFontSize(20); - doc.setFont('helvetica', 'bold'); - doc.text('Certificat de Validation de Document', 105, 80, { align: 'center' }); - + y -= 4 * lineSpacing; // Add more space between header and title + const titleText = 'Certificat de Validation de Document'; + const titleWidth = helveticaBoldFont.widthOfTextAtSize(titleText, 20); + page.drawText(titleText, { + x: (width - titleWidth) / 2, + y: y, + size: 20, + font: helveticaBoldFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + // Add a line separator - doc.setLineWidth(0.5); - doc.line(20, 90, 190, 90); - - // Reset font for content - doc.setFontSize(12); - doc.setFont('helvetica', 'normal'); - - let yPosition = 110; - + page.drawLine({ + start: { x: leftMargin, y: y }, + end: { x: width - leftMargin, y: y }, + thickness: 0.5, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + // Document Information Section - doc.setFont('helvetica', 'bold'); - doc.text('Informations du Document', 20, yPosition); - yPosition += 10; - - doc.setFont('helvetica', 'normal'); - doc.text(`UID du Document: ${certificateData.metadata.documentUid}`, 20, yPosition); - yPosition += 7; - doc.text(`Type de Document: ${certificateData.metadata.documentType}`, 20, yPosition); - yPosition += 7; - doc.text(`UID du Dossier: ${certificateData.folderUid}`, 20, yPosition); - yPosition += 7; - doc.text(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, 20, yPosition); - yPosition += 7; - doc.text(`Nom du fichier: ${certificateData.metadata.fileName}`, 20, yPosition); - yPosition += 7; - doc.text(`ID de transaction: ${certificateData.metadata.commitmentId}`, 20, yPosition); - yPosition += 7; - doc.text(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, 20, yPosition); - yPosition += 7; - doc.text(`Statut: ${certificateData.metadata.isDeleted ? 'Supprimé' : 'Actif'}`, 20, yPosition); - yPosition += 15; - + page.drawText('Informations du Document', { + x: leftMargin, + y: y, + size: 12, + font: helveticaBoldFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + page.drawText(`UID du Dossier: ${certificateData.folderUid}`, { + x: leftMargin, + y: y, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + page.drawText(`Type de Document: ${certificateData.metadata.documentType}`, { + x: leftMargin, + y: y, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + page.drawText(`Nom du fichier: ${certificateData.metadata.fileName}`, { + x: leftMargin, + y: y, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + page.drawText(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, { + x: leftMargin, + y: y, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + page.drawText(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, { + x: leftMargin, + y: y, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0) + }); + y -= lineSpacing; + // Document Hash Section - doc.setFont('helvetica', 'bold'); - doc.text('Intégrité du Document', 20, yPosition); - yPosition += 10; + 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)); - doc.setFont('helvetica', 'normal'); - doc.text(`Hash SHA256 du Document:`, 20, yPosition); - yPosition += 7; + // 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 - // Split hash into multiple lines if needed - const hash = certificateData.documentHash; - const maxWidth = 170; // Available width for text - const hashLines = this.splitTextToFit(doc, hash, maxWidth); - - for (const line of hashLines) { - doc.text(line, 25, yPosition); - yPosition += 7; + const chunks = []; + for (let j = 0; j < verificationDataBase64.length; j += chunkSize) { + chunks.push(verificationDataBase64.slice(j, j + chunkSize)); } - yPosition += 8; // Extra space after hash - + 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) + }); + }); + + // 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); + explanatoryLines.forEach((line, index) => { + page.drawText(line, { + x: leftMargin, + y: y - (index * 14), // Slightly more spacing for readability + size: 11, + font: helveticaObliqueFont, + color: rgb(0.3, 0.3, 0.3) // Slightly grayed out to distinguish from main content + }); + }); + // Footer - const pageCount = doc.getNumberOfPages(); - for (let i = 1; i <= pageCount; i++) { - doc.setPage(i); - doc.setFontSize(10); - doc.setFont('helvetica', 'italic'); - doc.text(`Page ${i} sur ${pageCount}`, 105, 280, { align: 'center' }); - doc.text(`Généré le ${new Date().toLocaleString('fr-FR')}`, 105, 285, { align: 'center' }); - } - - return doc.output('blob'); + const footerText1 = `Page 1 sur 1`; + const footerText2 = `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: 35, + 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) + }); + + const pdfBytes = await pdfDoc.save(); + return new Blob([pdfBytes], { type: 'application/pdf' }); } catch (error) { console.error('Error generating certificate:', error); throw new Error('Failed to generate certificate'); @@ -143,21 +269,22 @@ export default class PdfService { /** * Split text to fit within a specified width - * @param doc - jsPDF document instance + * @param font - PDF font instance * @param text - Text to split * @param maxWidth - Maximum width in points + * @param fontSize - Font size * @returns Array of text lines */ - private splitTextToFit(doc: jsPDF, text: string, maxWidth: number): string[] { + private splitTextToFit(font: any, text: string, maxWidth: number, fontSize: number): string[] { const lines: string[] = []; let currentLine = ''; - // Split by words first - const words = text.split(''); + // Split by characters (pdf-lib doesn't have word-level text measurement) + const chars = text.split(''); - for (const char of words) { + for (const char of chars) { const testLine = currentLine + char; - const testWidth = doc.getTextWidth(testLine); + const testWidth = font.widthOfTextAtSize(testLine, fontSize); if (testWidth <= maxWidth) { currentLine = testLine; @@ -202,92 +329,257 @@ export default class PdfService { */ public async generateNotaryCertificate(certificateData: CertificateData): Promise { try { - const doc = new jsPDF(); + // 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) - doc.setFontSize(12); - doc.setFont('helvetica', 'bold'); - doc.text('Notaire Validateur:', 20, 30); - doc.setFont('helvetica', 'normal'); - doc.text(certificateData.notary.name, 20, 37); + 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) - doc.setFont('helvetica', 'bold'); - doc.text('Fournisseur de Document:', 120, 30); - doc.setFont('helvetica', 'normal'); - doc.text(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, 120, 37); - doc.text(certificateData.customer.email, 120, 44); - doc.text(certificateData.customer.postalAddress, 120, 51); + 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 - doc.setFontSize(20); - doc.setFont('helvetica', 'bold'); - doc.text('Certificat Notarial de Validation de Document', 105, 80, { align: 'center' }); + 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 - doc.setLineWidth(0.5); - doc.line(20, 90, 190, 90); + page.drawLine({ + start: { x: 20, y: height - 90 }, + end: { x: width - 20, y: height - 90 }, + thickness: 0.5, + color: rgb(0, 0, 0) + }); - // Reset font for content - doc.setFontSize(12); - doc.setFont('helvetica', 'normal'); - - let yPosition = 110; + let yPosition = height - 110; // Document Information Section - doc.setFont('helvetica', 'bold'); - doc.text('Informations du Document', 20, yPosition); - yPosition += 10; + page.drawText('Informations du Document', { + x: 20, + y: yPosition, + size: 12, + font: helveticaBoldFont, + color: rgb(0, 0, 0) + }); + yPosition -= 10; - doc.setFont('helvetica', 'normal'); - doc.text(`Identifiant du Document: ${certificateData.metadata.documentUid}`, 20, yPosition); - yPosition += 7; - doc.text(`Type de Document: ${certificateData.metadata.documentType}`, 20, yPosition); - yPosition += 7; - doc.text(`Numéro de dossier: ${certificateData.folderUid}`, 20, yPosition); - yPosition += 7; - doc.text(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, 20, yPosition); - yPosition += 7; - doc.text(`Nom du fichier: ${certificateData.metadata.fileName}`, 20, yPosition); - yPosition += 7; - doc.text(`ID de transaction: ${certificateData.metadata.commitmentId}`, 20, yPosition); - yPosition += 7; - doc.text(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, 20, yPosition); - yPosition += 7; - doc.text(`Statut: ${certificateData.metadata.isDeleted ? 'Supprimé' : 'Actif'}`, 20, yPosition); - yPosition += 15; + 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 - doc.setFont('helvetica', 'bold'); - doc.text('Intégrité du Document', 20, yPosition); - yPosition += 10; + page.drawText('Intégrité du Document', { + x: 20, + y: yPosition, + size: 12, + font: helveticaBoldFont, + color: rgb(0, 0, 0) + }); + yPosition -= 10; - doc.setFont('helvetica', 'normal'); - doc.text(`Hash SHA256 du Document:`, 20, yPosition); - yPosition += 7; + 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(doc, hash, maxWidth); + const hashLines = this.splitTextToFit(helveticaFont, hash, maxWidth, 12); for (const line of hashLines) { - doc.text(line, 25, yPosition); - yPosition += 7; + 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 -= 8; // Extra space after hash + + yPosition -= 5; // Footer - const pageCount = doc.getNumberOfPages(); - for (let i = 1; i <= pageCount; i++) { - doc.setPage(i); - doc.setFontSize(10); - doc.setFont('helvetica', 'italic'); - doc.text(`Page ${i} sur ${pageCount}`, 105, 280, { align: 'center' }); - doc.text(`Certificat Notarial - Généré le ${new Date().toLocaleString('fr-FR')}`, 105, 285, { align: 'center' }); + 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)); } - return doc.output('blob'); + 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'); @@ -309,4 +601,141 @@ export default class PdfService { throw error; } } -} \ No newline at end of file + + /** + * 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}> { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + + 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'); + } + + resolve(parsedData); + } catch (error) { + reject(error); + } + }; + + fileReader.onerror = () => { + reject(new Error('Failed to read PDF file')); + }; + + fileReader.readAsArrayBuffer(pdfBlob); + }); + } + + /** + * 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; + } +}