/* Social-post export engine — generalization of the pipeline proven in fotografia.andmore.pt/views/sessions/open_spots_ig.php. Responsibilities: render a live-preview element to a high-resolution JPG via html2canvas, pre-baking CSS blur (html2canvas can't render `filter: blur()`), waiting for webfonts, and bundling multi-slide exports into a ZIP (JSZip). Pure mechanism — all template/DOM knowledge lives in the page that uses it. */ window.SocialEngine = (function () { 'use strict'; function chunk(arr, size) { size = Math.max(1, size | 0); var out = []; for (var i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); return out; } /* html2canvas does not render CSS `filter: blur(...)`. Pre-bake the blur into the photo with Canvas 2D, swap the bg-image to that bitmap, drop the inline filter/transform, and return a restore() callback. The blur radius is multiplied by the export scale so the upscaled bitmap matches what's on screen. Resolves null when there is nothing to bake. */ function bakeBlur(bgEl, photoUrl, blurAmount, scale) { return new Promise(function (resolve) { if (!bgEl || !photoUrl || !(blurAmount > 0)) return resolve(null); var rect = bgEl.getBoundingClientRect(); var W = Math.max(1, Math.round(rect.width * scale)); var H = Math.max(1, Math.round(rect.height * scale)); var blurPx = blurAmount * scale; var img = new Image(); // Photos go through the same-origin media proxy (or are local hub // uploads), so no crossOrigin attribute is needed and the canvas // stays untainted. img.onload = function () { try { var canvas = document.createElement('canvas'); canvas.width = W; canvas.height = H; var ctx = canvas.getContext('2d'); ctx.filter = 'blur(' + blurPx + 'px) brightness(0.85)'; // Draw slightly larger than the canvas so the soft edges // don't reveal blank pixels — same trick as the CSS scale(). var overscan = 1.1 + (blurAmount / 200); var ratio = Math.max(W / img.width, H / img.height) * overscan; var dw = img.width * ratio; var dh = img.height * ratio; ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh); var dataUrl = canvas.toDataURL('image/jpeg', 0.9); var original = { backgroundImage: bgEl.style.backgroundImage, filter: bgEl.style.filter, transform: bgEl.style.transform, }; bgEl.style.backgroundImage = "url('" + dataUrl + "')"; bgEl.style.filter = 'none'; bgEl.style.transform = 'none'; resolve(function () { bgEl.style.backgroundImage = original.backgroundImage; bgEl.style.filter = original.filter; bgEl.style.transform = original.transform; }); } catch (e) { console.warn('Blur bake failed; exporting without baked blur.', e); resolve(null); } }; img.onerror = function () { console.warn('Failed to load photo for blur bake; exporting without baked blur.'); resolve(null); }; img.src = photoUrl; }); } /* Render `el` (the live preview, at its on-screen size — its CSS uses viewport-relative clamp()s, so rendering at another width would re-flow) and let html2canvas upscale the pixel buffer to `targetW` via `scale`. opts.blur = { bgEl, photoUrl, amount } enables the bake step. Returns a JPEG Blob. */ async function renderBlob(el, targetW, opts) { opts = opts || {}; if (typeof html2canvas !== 'function') { throw new Error('A biblioteca de exportação ainda está a carregar. Tenta novamente em segundos.'); } var rect = el.getBoundingClientRect(); var actualW = rect.width || el.offsetWidth || targetW; var actualH = rect.height || el.offsetHeight || targetW; var scale = actualW > 0 ? (targetW / actualW) : 1; if (document.fonts && document.fonts.ready) { await document.fonts.ready; } var restore = null; if (opts.blur) { restore = await bakeBlur(opts.blur.bgEl, opts.blur.photoUrl, opts.blur.amount, scale); } try { var canvas = await html2canvas(el, { backgroundColor: '#000', scale: scale, useCORS: true, logging: false, width: actualW, height: actualH, windowWidth: document.documentElement.clientWidth, windowHeight: document.documentElement.clientHeight, }); return await new Promise(function (resolve, reject) { canvas.toBlob(function (blob) { if (!blob) { reject(new Error('Falha ao gerar a imagem.')); return; } resolve(blob); }, 'image/jpeg', 0.95); }); } finally { if (restore) restore(); } } function saveBlob(blob, fileName) { var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(function () { URL.revokeObjectURL(url); }, 1500); } /* files: [{ blob, name }]. One file downloads directly; several are bundled into a single ZIP. */ async function saveAll(files, zipName) { if (!files.length) return; if (files.length === 1) { saveBlob(files[0].blob, files[0].name); return; } if (typeof JSZip !== 'function') { // ZIP library missing — degrade to sequential downloads. files.forEach(function (f) { saveBlob(f.blob, f.name); }); return; } var zip = new JSZip(); files.forEach(function (f) { zip.file(f.name, f.blob); }); var blob = await zip.generateAsync({ type: 'blob' }); saveBlob(blob, zipName); } return { chunk: chunk, renderBlob: renderBlob, saveBlob: saveBlob, saveAll: saveAll, }; })();