// 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 (
{
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 && (
)}
);
};
/* EOF ========== [__react/socle/_socle_pdf.jsx] */