Files
fitme3070.pt/shared/platform-tools/social/engine.js
T
andreandClaude Opus 5 4d40adbf64 Vendor platform-tools: everything this site runs is now in one repo
shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.

The submodule caused every deployment failure this site has had:

  - .gitmodules named no `branch`, so the update fell back to
    refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
    "Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
    revision in submodule path".
  - Its URL was http://, which the origin answers with 523.
  - This install is deployed by uploading files, and a submodule is invisible
    to that. The tree moved forward while shared/platform-tools stayed on the
    old pin, config/functions_secrets.php died on a missing compat_secrets.php,
    and the whole site — public pages included — went down.

Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.

The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:06:23 +01:00

160 lines
6.7 KiB
JavaScript

/* 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,
};
})();