// ECO-PANNEAU.FR - __react/socle/_socle_pdf.jsx const { useState, useEffect, useRef, memo } = React; // 1. - VISIONNEUSES PDF (PIÈCES JOINTES ET ARRÊTÉS) window.pano_PdfThumbnail = memo(({ url, id }) => { const { LoaderIcon, FileIcon } = window.pano_getIcons(); const [error, setError] = useState(false); const previewUrl = id ? `${window.pano_CONFIG.apiBaseUrl}file/preview&type=pdf&id=${id}` : url?.replace('download', 'preview'); if (error) { return (
); } return (
Aperçu PDF { if (!error) { if (window.pano_logFallback) window.pano_logFallback(`Échec du chargement de la miniature PDF (id: ${id}). Affichage de l'icône de secours.`); setError(true); } }} />
); }); const PdfPage = memo(({ pdf, pageNumber }) => { const canvasRef = useRef(null); const [rendered, setRendered] = useState(false); const { LoaderIcon } = window.pano_getIcons(); useEffect(() => { let renderTask = null; let isSubscribed = true; pdf.getPage(pageNumber).then(page => { if (!isSubscribed) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const viewport = page.getViewport({ scale: 1.5 }); canvas.width = viewport.width; canvas.height = viewport.height; canvas.style.width = '100%'; canvas.style.maxWidth = `${viewport.width}px`; canvas.style.height = 'auto'; renderTask = page.render({ canvasContext: ctx, viewport }); renderTask.promise.then(() => { if (isSubscribed) setRendered(true); }).catch(err => { if (err.name !== 'RenderingCancelledException') { if (window.pano_logFallback) window.pano_logFallback(`Erreur de rendu PDF.js (page ${pageNumber}): ${err.message}`); } }); }); return () => { isSubscribed = false; if (renderTask) renderTask.cancel(); }; }, [pdf, pageNumber]); return (
{!rendered && }
); }); window.pano_PdfFullViewer = memo(({ url, id, isPrivate = false }) => { const { LoaderIcon, FileIcon } = window.pano_getIcons(); const [pdf, setPdf] = useState(null); const [error, setError] = useState(false); const [loading, setLoading] = useState(true); // CORRECTION OOM : Référence pour nettoyer la mémoire vive const pdfRef = useRef(null); const downloadUrl = id ? `${window.pano_CONFIG.apiBaseUrl}file/download&type=pdf&id=${id}${isPrivate ? '&private=1' : ''}` : url; useEffect(() => { let isMounted = true; if (!window.pano_pdfjsLib) { if (window.pano_logFallback) window.pano_logFallback("pano_PdfFullViewer: Bibliothèque PDF.js manquante. Affichage de l'erreur au client."); setError(true); setLoading(false); return; } const fetchAndRenderPdf = async () => { try { // CORRECTION OOM : Utilisation du streaming natif de PDF.js au lieu d'un fetch() en arrayBuffer complet const loadingTask = window.pano_pdfjsLib.getDocument({ url: downloadUrl, httpHeaders: { 'x-silent': '1' } }); const loadedPdf = await loadingTask.promise; if (isMounted) { pdfRef.current = loadedPdf; setPdf(loadedPdf); setLoading(false); } else { // Le composant a été fermé pendant le téléchargement loadedPdf.destroy(); } } catch (err) { if (window.pano_logFallback) window.pano_logFallback(`Échec de récupération ou lecture du PDF complet (${downloadUrl}): ${err.message}`); if (isMounted) { setError(true); setLoading(false); } } }; fetchAndRenderPdf(); return () => { isMounted = false; // CORRECTION OOM : Libération stricte de la mémoire du Web Worker pdf.js if (pdfRef.current) { pdfRef.current.destroy(); pdfRef.current = null; } }; }, [downloadUrl]); if (loading) { return (
Chargement du document...
); } if (error || !pdf) { return (
Aperçu indisponible ou document corrompu.
); } const pages = []; for (let i = 1; i <= pdf.numPages; i++) { pages.push(); } return (
{pages}
); }); // 2. - GÉNÉRATEUR PDF NATIF A1 (IMPRESSION) window.pano_generateA1PDF = async (panneau) => { const jsPDF = window.jspdf ? window.jspdf.jsPDF : null; if (!jsPDF) { if (window.pano_logFallback) window.pano_logFallback("Générateur A1: Bibliothèque jsPDF introuvable. Échec de la génération."); throw new Error("La bibliothèque de génération PDF n'est pas encore chargée."); } // FORMAT IMPRIMEUR EXACT AVEC FOND PERDU : 845 x 598 mm (Paysage) const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: [845, 598] }); let fontLoaded = false; const hexToRgb = (hex) => { let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex || '#059669'); if (result) { return { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }; } return { r: 5, g: 150, b: 105 }; }; // CORRECTION OPTIMISATION : Plafond à ~1200px (200 DPI), fond '#F8FAFC' et export JPEG 85% const loadImage = (url) => new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => { const MAX_WIDTH = 1200; let width = img.width; let height = img.height; if (width > MAX_WIDTH) { const ratio = MAX_WIDTH / width; width = MAX_WIDTH; height = height * ratio; } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); // Fond plein gris clair (slate-50 de Tailwind : rgb(248, 250, 252)) ctx.fillStyle = '#F8FAFC'; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); // Export en JPEG 0.85 resolve({ dataUrl: canvas.toDataURL('image/jpeg', 0.85), width: width, height: height }); }; img.onerror = reject; img.src = url; }); // CORRECTION OPTIMISATION : Fond plein foncé '#0F172A' et export JPEG 85% const loadFaviconAsHighResJpeg = async (url) => { try { const resp = await fetch(url); if (!resp.ok) return null; let svgText = await resp.text(); if (!svgText.includes('width=')) { svgText = svgText.replace(' { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = 2048; canvas.height = 2048; const ctx = canvas.getContext('2d'); // Fond plein bleu nuit (slate-900 de Tailwind : rgb(15, 23, 42)) ctx.fillStyle = '#0F172A'; ctx.fillRect(0, 0, 2048, 2048); ctx.drawImage(img, 0, 0, 2048, 2048); // Export en JPEG 0.85 resolve({ dataUrl: canvas.toDataURL('image/jpeg', 0.85), width: 2048, height: 2048 }); }; img.onerror = reject; img.src = svgBase64; }); } catch (e) { return null; } }; let moaLogo = null; let faviconLogo = null; try { const loadFont = async (fontName, fontStyle, url) => { const cacheKey = `${fontName}-${fontStyle}`; let base64 = window.pano_CONFIG?.pdfFontsCache?.[cacheKey]; if (!base64) { const resp = await fetch(url); if (!resp.ok) { throw new Error("Erreur réseau lors du chargement de la police"); } const buffer = await resp.arrayBuffer(); let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } base64 = window.btoa(binary); if (!window.pano_CONFIG) window.pano_CONFIG = {}; if (!window.pano_CONFIG.pdfFontsCache) window.pano_CONFIG.pdfFontsCache = {}; window.pano_CONFIG.pdfFontsCache[cacheKey] = base64; } const fileName = `${cacheKey}.ttf`; doc.addFileToVFS(fileName, base64); doc.addFont(fileName, fontName, fontStyle); }; const assetsUrl = (window.pano_CONFIG && window.pano_CONFIG.assetsUrl) ? window.pano_CONFIG.assetsUrl : '_assets/'; await Promise.all([ loadFont("Roboto", "normal", window.pano_CONFIG?.fonts?.robotoRegular || (assetsUrl + "roboto/Roboto-Regular.ttf")), loadFont("Roboto", "bold", window.pano_CONFIG?.fonts?.robotoMedium || (assetsUrl + "roboto/Roboto-Medium.ttf")) ]); fontLoaded = true; const logoUrl = panneau.maitreOuvrageLogoUrl || (panneau.maitreOuvrageLogoId ? `${window.pano_CONFIG?.apiBaseUrl || '?api='}file/download&type=image&id=${panneau.maitreOuvrageLogoId}` : null); if (logoUrl) { moaLogo = await loadImage(logoUrl); } const faviconUrl = window.location.origin + '/favicon.svg'; faviconLogo = await loadFaviconAsHighResJpeg(faviconUrl); } catch(e) { if (window.pano_logFallback) window.pano_logFallback(`Avertissement A1: Échec de chargement d'une ressource. Fallback système utilisé. Détail: ${e.message}`); } const sanitizeForPDF = (str) => { if (!str) { return ''; } if (fontLoaded) { return str.toString().replace(/[\u1000-\uFFFF]/g, ''); } let s = str.toString() .replace(/[’‘`]/g, "'") .replace(/[“”«»]/g, '"') .replace(/[–—]/g, '-') .replace(/œ/g, 'oe') .replace(/Œ/g, 'Oe') .replace(/€/g, 'EUR'); return s.replace(/[^\x00-\xFF]/g, ""); }; const mainFont = fontLoaded ? "Roboto" : "helvetica"; const themeRgb = hexToRgb(panneau.themeColor); doc.setFillColor(255, 255, 255); doc.rect(0, 0, 845, 598, 'F'); doc.setFillColor(themeRgb.r, themeRgb.g, themeRgb.b); doc.rect(0, 0, 845, 45, 'F'); if (!panneau.hasNoAds) { doc.setFillColor(15, 23, 42); doc.rect(0, 598 - 45, 845, 45, 'F'); doc.setFillColor(themeRgb.r, themeRgb.g, themeRgb.b); doc.rect(0, 598 - 50, 845, 5, 'F'); doc.setFont(mainFont, "bold"); doc.setFontSize(55); const textEco = "eco"; const textPanneau = "-panneau"; const textFr = ".fr"; const textSep = " | "; const textDesc = "Professionnels du BTP : digitalisez vos obligations."; const wEco = doc.getTextWidth(textEco); const wPanneau = doc.getTextWidth(textPanneau); const wFr = doc.getTextWidth(textFr); const wSep = doc.getTextWidth(textSep); const wDesc = doc.getTextWidth(textDesc); const iconSize = 18; const iconMargin = 6; const totalWidth = iconSize + iconMargin + wEco + wPanneau + wFr + wSep + wDesc; let startX = 422.5 - (totalWidth / 2); const textY = 598 - 16; const iconY = 598 - 45 + ((45 - iconSize) / 2); if (faviconLogo) { // INTEGRATION jsPDF : Utilisation stricte de JPEG compressé doc.addImage(faviconLogo.dataUrl, 'JPEG', startX, iconY, iconSize, iconSize); } else { doc.setFillColor(30, 41, 59); doc.roundedRect(startX, iconY, iconSize, iconSize, 3.5, 3.5, 'F'); doc.setDrawColor(255, 255, 255); doc.setLineWidth(1); doc.setFillColor(255, 255, 255); doc.roundedRect(startX + 2.5, iconY + 2.5, 5, 5, 1, 1, 'D'); doc.rect(startX + 4, iconY + 4, 2, 2, 'F'); doc.roundedRect(startX + 2.5, iconY + 10, 5, 5, 1, 1, 'D'); doc.rect(startX + 4, iconY + 11.5, 2, 2, 'F'); doc.roundedRect(startX + 10, iconY + 10, 5, 5, 1, 1, 'D'); doc.rect(startX + 11.5, iconY + 11.5, 2, 2, 'F'); doc.setFillColor(5, 150, 105); doc.triangle(startX + 10, iconY + 7.5, startX + 15.5, iconY + 2, startX + 15.5, iconY + 7.5, 'F'); } startX += iconSize + iconMargin; doc.setTextColor(5, 150, 105); doc.text(textEco, startX, textY); startX += wEco; doc.setTextColor(255, 255, 255); doc.text(textPanneau, startX, textY); startX += wPanneau; doc.setTextColor(148, 163, 184); doc.text(textFr, startX, textY); startX += wFr; doc.setTextColor(71, 85, 105); doc.text(textSep, startX, textY); startX += wSep; doc.setTextColor(255, 255, 255); doc.text(textDesc, startX, textY); } const qrSize = 210; const qrX = 55; const qrY = 110; doc.setDrawColor(226, 232, 240); doc.setLineWidth(3); doc.setFillColor(255, 255, 255); doc.roundedRect(qrX - 15, qrY - 15, qrSize + 30, qrSize + 30, 5, 5, 'FD'); if (!window.QRCode) { if (window.pano_logFallback) window.pano_logFallback("Générateur A1: QRCode.js manquant. Échec de la génération."); throw new Error("La bibliothèque de génération de QR Code n'est pas chargée."); } const qr = new window.QRCode({ content: `https://eco-panneau.fr/?scan=${panneau.id}`, padding: 0, width: 256, height: 256, ecl: "H" }); const modules = qr.qrcode.modules; const moduleCount = modules.length; const moduleSize = qrSize / moduleCount; doc.setFillColor(15, 23, 42); for (let row = 0; row < moduleCount; row++) { for (let col = 0; col < moduleCount; col++) { if (modules[row][col]) { doc.rect(qrX + col * moduleSize, qrY + row * moduleSize, moduleSize + 0.5, moduleSize + 0.5, 'F'); } } } doc.setTextColor(15, 23, 42); doc.setFontSize(40); doc.setFont(mainFont, "bold"); doc.text("Scannez pour les informations", qrX + qrSize / 2, qrY + qrSize + 50, { align: 'center' }); doc.text("utiles et légales.", qrX + qrSize / 2, qrY + qrSize + 70, { align: 'center' }); doc.setDrawColor(226, 232, 240); doc.setLineWidth(2); doc.line(345, 80, 345, panneau.hasNoAds ? 550 : 500); const drawAutoText = (text, x, y, maxW, maxH, startSize, minSize, fontName, fontStyle, color) => { const cleanText = sanitizeForPDF(text); if (!cleanText || cleanText.trim() === '') return { height: 0 }; doc.setFont(fontName, fontStyle); doc.setTextColor(color[0], color[1], color[2]); let fontSize = startSize; doc.setFontSize(fontSize); let lines = doc.splitTextToSize(cleanText, maxW); let loopCount = 0; while (lines.length * (fontSize * 0.3528 * 1.15) > maxH && fontSize > minSize) { loopCount++; if (loopCount > 30) break; fontSize -= 2; doc.setFontSize(fontSize); lines = doc.splitTextToSize(cleanText, maxW); } doc.text(lines, x, y); return { height: lines.length * (fontSize * 0.3528 * 1.15) }; }; const textX = 390; const maxTextW = 410; doc.setTextColor(themeRgb.r, themeRgb.g, themeRgb.b); doc.setFontSize(45); doc.setFont(mainFont, "bold"); doc.text("Panneau d'affichage légal", textX, 110); const nameMetrics = drawAutoText(panneau.name, textX, 150, maxTextW, 120, 100, 30, mainFont, "bold", [15, 23, 42]); const locY = Math.max(230, 150 + nameMetrics.height + 10); const locMetrics = drawAutoText(panneau.location, textX, locY, maxTextW, 60, 50, 20, mainFont, "normal", [71, 85, 105]); let boxY = Math.max(380, locY + locMetrics.height + 30); if (panneau.maitreOuvrage || moaLogo) { const moaText = panneau.maitreOuvrage || ''; doc.setFontSize(50); const availableTextWidth = moaLogo ? 260 : 380; let moaLines = doc.splitTextToSize(sanitizeForPDF(moaText), availableTextWidth); let moaSize = 50; if (moaLines.length > 1) { moaSize = 40; doc.setFontSize(40); moaLines = doc.splitTextToSize(sanitizeForPDF(moaText), availableTextWidth); } if (moaLines.length > 2) { moaSize = 30; doc.setFontSize(30); moaLines = doc.splitTextToSize(sanitizeForPDF(moaText), availableTextWidth); } let textH = moaText ? (moaLines.length * (moaSize * 0.3528 * 1.15)) : 0; let contentH = Math.max(textH, moaLogo ? 60 : 0); let boxHeight = Math.max(70, 30 + contentH + 10); doc.setFillColor(248, 250, 252); doc.setDrawColor(226, 232, 240); doc.roundedRect(textX, boxY, 410, boxHeight, 5, 5, 'FD'); doc.setTextColor(100, 116, 139); doc.setFontSize(30); doc.setFont(mainFont, "bold"); doc.text("Maître d'ouvrage", textX + 15, boxY + 22); if (moaText) { doc.setTextColor(15, 23, 42); doc.setFontSize(moaSize); doc.text(moaLines, textX + 15, boxY + 48); } if (moaLogo) { const maxLogoW = 110; const maxLogoH = boxHeight - 20; const ratio = Math.min(maxLogoW / moaLogo.width, maxLogoH / moaLogo.height); const logoW = moaLogo.width * ratio; const logoH = moaLogo.height * ratio; const logoX = textX + 410 - 15 - logoW; const logoY = boxY + 10 + (maxLogoH - logoH) / 2; // INTEGRATION jsPDF : Utilisation stricte de JPEG compressé doc.addImage(moaLogo.dataUrl, 'JPEG', logoX, logoY, logoW, logoH); } boxY += boxHeight + 20; } if (panneau.permitNumber) { doc.setTextColor(148, 163, 184); doc.setFontSize(35); doc.setFont(mainFont, "bold"); doc.text("Permis N°", textX, boxY + 25); drawAutoText(panneau.permitNumber.toString(), textX + 100, boxY + 25, 310, 60, 60, 20, mainFont, "bold", [15, 23, 42]); } return doc.output('blob'); }; // 3. - PRÉVISUALISATION A1 window.pano_PrintA1View = ({ panneau }) => { const [isGenerating, setIsGenerating] = React.useState(true); const [error, setError] = React.useState(null); const canvasRef = React.useRef(null); const { LoaderIcon, AlertTriangleIcon } = window.pano_getIcons(); const panelFingerprint = JSON.stringify({ id: panneau?.id, name: panneau?.name, location: panneau?.location, themeColor: panneau?.themeColor, hasNoAds: panneau?.hasNoAds, maitreOuvrage: panneau?.maitreOuvrage, maitreOuvrageLogoId: panneau?.maitreOuvrageLogoId, permitNumber: panneau?.permitNumber }); React.useEffect(() => { let isMounted = true; let currentPdf = null; // CORRECTION OOM : Référence locale const renderPDF = async () => { if (!panneau) return; try { setIsGenerating(true); setError(null); const blob = await window.pano_generateA1PDF(panneau); if (!isMounted) return; if (!window.pano_pdfjsLib) { if (window.pano_logFallback) window.pano_logFallback("PrintA1View: La bibliothèque de lecture PDF.js n'est pas chargée. Échec de l'aperçu."); throw new Error("La bibliothèque de lecture PDF n'est pas chargée."); } const arrayBuffer = await blob.arrayBuffer(); currentPdf = await window.pano_pdfjsLib.getDocument({ data: arrayBuffer }).promise; const page = await currentPdf.getPage(1); if (!isMounted) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); // CORRECTION CRITIQUE (CRASH iOS CANVAS) : Calcul dynamique de la résolution maximale const baseVp = page.getViewport({ scale: 1 }); const safeScale = Math.min(2, 2048 / baseVp.width); // Limite stricte de largeur de 2048px const viewport = page.getViewport({ scale: safeScale }); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; if (isMounted) { setIsGenerating(false); } } catch (err) { if (window.pano_logFallback) window.pano_logFallback(`Erreur de génération de l'aperçu PDF A1: ${err.message}`); if (isMounted) { setError(err.message); setIsGenerating(false); } } finally { // CORRECTION OOM : On nettoie l'objet lourd une fois rendu if (currentPdf) { currentPdf.destroy(); currentPdf = null; } } }; renderPDF(); return () => { isMounted = false; // Filet de sécurité en cas de démontage avant la fin if (currentPdf) { currentPdf.destroy(); currentPdf = null; } }; }, [panelFingerprint]); return (
{isGenerating && (
Rendu haute fidélité en cours...
)} {error && (
Erreur d'aperçu {error}
)}
); }; /* EOF ========== [__react/socle/_socle_pdf.jsx] */