Malaji71 commited on
Commit
d69ee52
·
verified ·
1 Parent(s): a1b9ec3

fix(pdf): inlinar imagenes como data: URL para evitar canvas tainted

Browse files

Las 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

Files changed (1) hide show
  1. 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
- // Convertir bg a base64
 
2912
  const bgStyle = window.getComputedStyle(slide).backgroundImage;
2913
  const urlMatch = bgStyle.match(/url\(["']?(.*?)["']?\)/);
2914
- if (urlMatch && urlMatch[1]) {
2915
  try {
2916
- const res = await fetch(urlMatch[1]);
2917
- const blob = await res.blob();
2918
- const b64 = await new Promise(r => {
2919
- const rd = new FileReader();
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(canvas.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, 1920, 1080, undefined, 'FAST');
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';