Spaces:
Running
Running
fix(pdf): inlinar imagenes como data: URL para evitar canvas tainted
Browse filesLas imagenes son rutas same-origin, pero HF responde con un 302 al CDN
(us.aws.cdn.hf.co). html2canvas solo pone crossOrigin cuando la URL ya es
cross-origin (useCORS && !isSameOrigin), asi que cargaba sin CORS, seguia el
redirect y contaminaba el canvas -> toDataURL() fallaba con
"Tainted canvases may not be exported".
- inlineImage(): fetch -> blob -> data: URL, cacheado por URL
- se inlinean el background del slide y todas sus <img> antes de capturar,
y se restauran despues
- toDataURL() por pagina informa que diapositiva falla
- si la generacion falla, el deck vuelve a ser usable en vez de quedarse oculto
- index.html +56 -11
index.html
CHANGED
|
@@ -2841,6 +2841,30 @@
|
|
| 2841 |
if (e.key === 'ArrowLeft') changeSlide(-1);
|
| 2842 |
});
|
| 2843 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2844 |
// ===================== GENERADOR DE PDF - FORZADO DESKTOP =====================
|
| 2845 |
async function generatePDF() {
|
| 2846 |
const loadingOverlay = document.getElementById('pdf-loading');
|
|
@@ -2908,21 +2932,30 @@
|
|
| 2908 |
document.head.appendChild(tempMeta);
|
| 2909 |
}
|
| 2910 |
|
| 2911 |
-
//
|
|
|
|
| 2912 |
const bgStyle = window.getComputedStyle(slide).backgroundImage;
|
| 2913 |
const urlMatch = bgStyle.match(/url\(["']?(.*?)["']?\)/);
|
| 2914 |
-
if (urlMatch && urlMatch[1]) {
|
| 2915 |
try {
|
| 2916 |
-
const
|
| 2917 |
-
const
|
| 2918 |
-
|
| 2919 |
-
|
| 2920 |
-
rd.onloadend = () => r(rd.result);
|
| 2921 |
-
rd.readAsDataURL(blob);
|
| 2922 |
-
});
|
| 2923 |
-
slide.style.backgroundImage = `url(${b64})`;
|
| 2924 |
} catch (e) { console.warn("BG conversion failed:", e); }
|
| 2925 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2926 |
|
| 2927 |
// Capturar
|
| 2928 |
const canvas = await html2canvas(slide, {
|
|
@@ -2943,6 +2976,8 @@
|
|
| 2943 |
removeContainer: true
|
| 2944 |
});
|
| 2945 |
|
|
|
|
|
|
|
| 2946 |
// === RESTAURAR viewport original DESPUÉS de capturar ===
|
| 2947 |
await new Promise(resolve => setTimeout(resolve, 100));
|
| 2948 |
if (originalMetaViewport) {
|
|
@@ -2955,8 +2990,14 @@
|
|
| 2955 |
}
|
| 2956 |
}
|
| 2957 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2958 |
if (i > 0) pdf.addPage();
|
| 2959 |
-
pdf.addImage(
|
| 2960 |
|
| 2961 |
links.forEach(link => {
|
| 2962 |
pdf.link(link.x, link.y, link.width, link.height, { url: link.url });
|
|
@@ -2980,6 +3021,10 @@
|
|
| 2980 |
|
| 2981 |
} catch (err) {
|
| 2982 |
console.error('Error:', err);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2983 |
alert('Error al generar PDF: ' + err.message);
|
| 2984 |
} finally {
|
| 2985 |
loadingOverlay.style.display = 'none';
|
|
|
|
| 2841 |
if (e.key === 'ArrowLeft') changeSlide(-1);
|
| 2842 |
});
|
| 2843 |
|
| 2844 |
+
// Las imagenes del deck son rutas same-origin, pero HF responde con un 302 hacia
|
| 2845 |
+
// el CDN (us.aws.cdn.hf.co). html2canvas solo aplica crossOrigin cuando la URL YA
|
| 2846 |
+
// es cross-origin (useCORS && !isSameOrigin), asi que las carga sin CORS, sigue el
|
| 2847 |
+
// redirect y contamina el canvas: toDataURL() lanza "Tainted canvases may not be
|
| 2848 |
+
// exported". Inlinearlas como data: URL elimina el problema de raiz.
|
| 2849 |
+
const pdfImageCache = new Map();
|
| 2850 |
+
function inlineImage(url) {
|
| 2851 |
+
if (!url || url.startsWith('data:')) return Promise.resolve(url);
|
| 2852 |
+
if (!pdfImageCache.has(url)) {
|
| 2853 |
+
pdfImageCache.set(url, (async () => {
|
| 2854 |
+
const res = await fetch(url);
|
| 2855 |
+
if (!res.ok) throw new Error(res.status + ' ' + url);
|
| 2856 |
+
const blob = await res.blob();
|
| 2857 |
+
return await new Promise((resolve, reject) => {
|
| 2858 |
+
const rd = new FileReader();
|
| 2859 |
+
rd.onloadend = () => resolve(rd.result);
|
| 2860 |
+
rd.onerror = () => reject(rd.error);
|
| 2861 |
+
rd.readAsDataURL(blob);
|
| 2862 |
+
});
|
| 2863 |
+
})());
|
| 2864 |
+
}
|
| 2865 |
+
return pdfImageCache.get(url);
|
| 2866 |
+
}
|
| 2867 |
+
|
| 2868 |
// ===================== GENERADOR DE PDF - FORZADO DESKTOP =====================
|
| 2869 |
async function generatePDF() {
|
| 2870 |
const loadingOverlay = document.getElementById('pdf-loading');
|
|
|
|
| 2932 |
document.head.appendChild(tempMeta);
|
| 2933 |
}
|
| 2934 |
|
| 2935 |
+
// Inlinear bg del slide + todas sus <img> como data: URL (ver inlineImage)
|
| 2936 |
+
const restoreImages = [];
|
| 2937 |
const bgStyle = window.getComputedStyle(slide).backgroundImage;
|
| 2938 |
const urlMatch = bgStyle.match(/url\(["']?(.*?)["']?\)/);
|
| 2939 |
+
if (urlMatch && urlMatch[1] && !urlMatch[1].startsWith('data:')) {
|
| 2940 |
try {
|
| 2941 |
+
const b64 = await inlineImage(urlMatch[1]);
|
| 2942 |
+
const prevBg = slide.style.backgroundImage;
|
| 2943 |
+
restoreImages.push(() => { slide.style.backgroundImage = prevBg; });
|
| 2944 |
+
slide.style.backgroundImage = `url("${b64}")`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2945 |
} catch (e) { console.warn("BG conversion failed:", e); }
|
| 2946 |
}
|
| 2947 |
+
for (const img of slide.querySelectorAll('img[src]')) {
|
| 2948 |
+
if (img.src.startsWith('data:')) continue;
|
| 2949 |
+
try {
|
| 2950 |
+
const b64 = await inlineImage(img.src);
|
| 2951 |
+
const prevSrc = img.getAttribute('src');
|
| 2952 |
+
restoreImages.push(() => { img.setAttribute('src', prevSrc); });
|
| 2953 |
+
img.src = b64;
|
| 2954 |
+
if (!img.complete) {
|
| 2955 |
+
await new Promise(r => { img.onload = r; img.onerror = r; });
|
| 2956 |
+
}
|
| 2957 |
+
} catch (e) { console.warn("IMG conversion failed:", img.src, e); }
|
| 2958 |
+
}
|
| 2959 |
|
| 2960 |
// Capturar
|
| 2961 |
const canvas = await html2canvas(slide, {
|
|
|
|
| 2976 |
removeContainer: true
|
| 2977 |
});
|
| 2978 |
|
| 2979 |
+
restoreImages.forEach(fn => fn());
|
| 2980 |
+
|
| 2981 |
// === RESTAURAR viewport original DESPUÉS de capturar ===
|
| 2982 |
await new Promise(resolve => setTimeout(resolve, 100));
|
| 2983 |
if (originalMetaViewport) {
|
|
|
|
| 2990 |
}
|
| 2991 |
}
|
| 2992 |
|
| 2993 |
+
let pageData;
|
| 2994 |
+
try {
|
| 2995 |
+
pageData = canvas.toDataURL('image/jpeg', 0.95);
|
| 2996 |
+
} catch (e) {
|
| 2997 |
+
throw new Error(`Could not export slide ${i + 1}: ${e.message}`);
|
| 2998 |
+
}
|
| 2999 |
if (i > 0) pdf.addPage();
|
| 3000 |
+
pdf.addImage(pageData, 'JPEG', 0, 0, 1920, 1080, undefined, 'FAST');
|
| 3001 |
|
| 3002 |
links.forEach(link => {
|
| 3003 |
pdf.link(link.x, link.y, link.width, link.height, { url: link.url });
|
|
|
|
| 3021 |
|
| 3022 |
} catch (err) {
|
| 3023 |
console.error('Error:', err);
|
| 3024 |
+
slides.forEach((s, idx) => {
|
| 3025 |
+
s.classList.remove('capture-mode');
|
| 3026 |
+
s.style.display = idx === originalSlide ? 'flex' : 'none';
|
| 3027 |
+
});
|
| 3028 |
alert('Error al generar PDF: ' + err.message);
|
| 3029 |
} finally {
|
| 3030 |
loadingOverlay.style.display = 'none';
|