/* The ONE source of truth for photo uploads — limits, validation and downscale. Two forms call this: the directory application form (/odoo-community-developers/) and the private self-service edit form (/freelancer-edit/). The limits live here once, so the two cannot drift. Loaded before each page's own script. Why this exists beyond page weight: re-encoding to a canvas DROPS EXIF entirely. A phone photo carries GPS coordinates, so an un-stripped portrait publishes the photographer's home location. Re-encoding is also a security step — a file whose declared type was spoofed fails to decode and is refused here. NOTE: the attribute is a picker HINT, never validation. Only validate() below decides, and it runs on the File object before any read. */ (function () { var MAX_INPUT_BYTES = 8 * 1024 * 1024; // 8 MB — refuse before reading a byte var ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; var MAX_SIDE = 384; // longest side of the stored image, px var MAX_OUTPUT_BYTES = 80 * 1024; // 80 KB — the encoded JPEG target var QUALITY = 0.82; // starting JPEG quality var QUALITY_STEP = 0.05; // step down until the target is met var QUALITY_FLOOR = 0.4; // never go below this function kb(bytes) { return Math.round(bytes / 1024); } function mb(bytes) { return (bytes / (1024 * 1024)).toFixed(1); } /* Decide on the File's own metadata — no read happens here. */ function validate(file) { if (!file) return { ok: false, reason: "No file was chosen." }; if (ALLOWED_TYPES.indexOf(file.type) === -1) { return { ok: false, reason: "That file type is not allowed. Use a JPEG, PNG or WebP image." }; } if (file.size > MAX_INPUT_BYTES) { return { ok: false, reason: "That photo is " + mb(file.size) + " MB. The limit is 8 MB — please choose a smaller one." }; } return { ok: true }; } function readAsDataUrl(file) { return new Promise(function (resolve, reject) { var reader = new FileReader(); reader.onload = function () { resolve(reader.result); }; reader.onerror = reject; reader.readAsDataURL(file); }); } function loadImage(url) { return new Promise(function (resolve, reject) { var img = new Image(); img.onload = function () { resolve(img); }; img.onerror = reject; // a spoofed "image/jpeg" that is not an image lands here img.src = url; }); } function byteLength(dataUrl) { return atob(dataUrl.slice(dataUrl.indexOf(",") + 1)).length; } /* Read → centre-crop square → cap longest side → encode JPEG → step quality down until it fits. Resolves { dataUrl, width, height, bytes, quality }. */ function downscale(file) { return readAsDataUrl(file).then(loadImage).then(function (img) { var w = img.naturalWidth, h = img.naturalHeight; var side = Math.min(w, h); var sx = (w - side) / 2, sy = (h - side) / 2; var out = Math.min(MAX_SIDE, side); var canvas = document.createElement("canvas"); canvas.width = out; canvas.height = out; canvas.getContext("2d").drawImage(img, sx, sy, side, side, 0, 0, out, out); var quality = QUALITY; var dataUrl = canvas.toDataURL("image/jpeg", quality); var bytes = byteLength(dataUrl); while (bytes > MAX_OUTPUT_BYTES && quality > QUALITY_FLOOR) { quality = Math.max(QUALITY_FLOOR, quality - QUALITY_STEP); dataUrl = canvas.toDataURL("image/jpeg", quality); bytes = byteLength(dataUrl); } return { dataUrl: dataUrl, width: out, height: out, bytes: bytes, quality: quality }; }); } /* The one honest line the person sees. No "optimised for the web" — only what we can show. */ function resultLine(res) { return "Resized to " + res.width + " × " + res.height + ", " + kb(res.bytes) + " KB"; } /* Wire one file input to a preview and a status line. onResult receives the downscaled data URL, or null when there is no (valid) file. Both forms use this, so the outcome shown is identical. */ function wire(input, preview, result, onResult) { if (!input) return; function say(text, state) { if (!result) return; result.textContent = text || ""; if (state) result.setAttribute("data-state", state); else result.removeAttribute("data-state"); } input.addEventListener("change", function () { var file = input.files && input.files[0]; if (!file) { if (preview) preview.hidden = true; say("", null); onResult(null); return; } var v = validate(file); if (!v.ok) { if (preview) preview.hidden = true; input.value = ""; // a refused file must never be submitted say(v.reason, "err"); onResult(null); return; } say("Processing…", null); downscale(file).then(function (res) { if (preview) { preview.src = res.dataUrl; preview.hidden = false; } say(resultLine(res), "ok"); onResult(res.dataUrl, res); }).catch(function () { if (preview) preview.hidden = true; input.value = ""; say("That file could not be read as an image. Please choose a JPEG, PNG or WebP.", "err"); onResult(null); }); }); } window.O4PPhoto = { MAX_INPUT_BYTES: MAX_INPUT_BYTES, ALLOWED_TYPES: ALLOWED_TYPES, MAX_SIDE: MAX_SIDE, MAX_OUTPUT_BYTES: MAX_OUTPUT_BYTES, QUALITY: QUALITY, validate: validate, downscale: downscale, resultLine: resultLine, kb: kb, wire: wire }; })();