web-files-image-handling-14eeaf — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-files-image-handling-14eeaf (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Two different jobs with two different tools. Displaying an image meansURL.createObjectURL()and revoking it afterwards — the browser already applies EXIF rotation, so rotating manually rotates twice. Processing an image means Canvas: clamp to 4096px, scale in multiple passes for reductions past 50%, fill white before writing JPEG, and prefer the asynctoBlob()overtoDataURL().
Detailed Resources:
loadImage helper, preview hook and component, dimension extraction and validation, EXIF parsing, gallery stateThe two paths disagree about EXIF, which is where most of the bugs are.
<img>, revoked when it is replaced orunmounted. The browser rotates for you; do nothing about orientation. Start at examples/core.md.
rotates for you, so normalise orientation explicitly and never display the result without suppressing the browser's own rotation. Start at examples/canvas.md.
<critical_requirements>
Revoke every object URL — on unmount, and before creating the replacement. Each createObjectURL pins its blob in memory until revoked, so a picker the user changes their mind in leaks a full image per attempt.
Decide which path you are on before touching orientation. Browsers have defaulted to image-orientation: from-image since 2020, so normalising for display rotates the image twice; normalise only for bytes that leave the browser.
Clamp canvas dimensions to 4096px. Past that the canvas fails silently or takes the tab down, and the limit is lower again on mobile.
Scale in multiple passes when reducing by more than half. One-pass downsampling undersamples and produces a soft, aliased result; two intermediate steps keep it sharp.
</critical_requirements>
Auto-detection: URL.createObjectURL, URL.revokeObjectURL, createImageBitmap, OffscreenCanvas, canvas.toBlob, canvas.toDataURL, convertToBlob, ctx.drawImage, imageSmoothingQuality, image-orientation, EXIF orientation, 0x0112, FileReader.readAsDataURL, naturalWidth, image/webp, step-down scaling, thumbnail generation, client-side resize, image crop
Applies to:
Handled elsewhere:
File or Blob you already holdrather than raw Canvas calls
<philosophy>
Preview and processing pull in opposite directions, and conflating them is the source of most image bugs.
A preview should cost nothing: URL.createObjectURL() hands <img> a reference to bytes that are already in memory, with no decode into JavaScript and no copy. The price is manual lifetime management — the reference outlives the component unless revoked.
Processing is the opposite: the pixels have to be decoded into a canvas, transformed, and encoded back out. Everything expensive lives here, which is why the canvas work is worth doing once, at the size you actually need, rather than repeatedly at full resolution.
The browser's own EXIF handling sits across the seam. It rotates for display and not for canvas, so the same file has two orientations depending on which path read it.
</philosophy>
<patterns>
Revoke the previous URL before creating the next one, and revoke on unmount. Creating a URL during render leaks one per render.
useEffect(() => {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
return () => URL.revokeObjectURL(url);
}, [file]);Full code: examples/core.md
Clamp to the browser limit, keep the aspect ratio, ask for high-quality smoothing, and fill white before drawing when the output is JPEG — JPEG has no alpha channel, so transparency encodes as black.
const MAX_CANVAS_DIMENSION = 4096;
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height);
const width = Math.round(img.width * Math.min(ratio, 1)); // never upscale
const height = Math.round(img.height * Math.min(ratio, 1));
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
if (mimeType === "image/jpeg") {
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
}
ctx.drawImage(img, 0, 0, width, height);Full code: examples/canvas.md
drawImage samples a fixed neighbourhood, so a large reduction in one pass throws away most of the source. Halving repeatedly keeps every pixel contributing.
const STEP_DOWN_THRESHOLD = 0.5;
if (targetWidth / img.width < STEP_DOWN_THRESHOLD) {
// 4000 → 400 → 100 rather than 4000 → 100
const factor = Math.pow(targetWidth / img.width, 1 / steps);
for (let i = 0; i < steps; i++) {
/* draw into a canvas scaled by factor, then use it as the next source */
}
}Full code: examples/canvas.md
// Display: the browser has already rotated it
<img src={URL.createObjectURL(file)} />
// Outbound bytes: normalise, because the next consumer may not rotate
const orientation = await getExifOrientation(file);
const normalized = orientation === 1 ? file : await normalizeOrientation(file);
// Displaying an already-normalised image: suppress the second rotation
<img src={url} style={{ imageOrientation: "none" }} />Full code: examples/core.md
Quality means something different per format, and PNG ignores the parameter entirely.
const FORMAT_QUALITY_DEFAULTS: Record<string, number> = {
"image/jpeg": 0.85,
"image/webp": 0.82,
"image/png": 1, // lossless — the quality argument is ignored
};WebP encodes everywhere modern, including Safari 14 and later. To hit a byte budget rather than a quality setting, binary-search the quality parameter.
Full code: examples/canvas.md
drawImage with nine arguments takes a source rectangle and a destination rectangle, so a crop and a resize are one call. Clamp the source rectangle to the image first — an out-of-bounds region draws nothing rather than erroring.
// drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh)
ctx.drawImage(
img,
cropX,
cropY,
cropWidth,
cropHeight,
0,
0,
outputWidth,
outputHeight,
);Full code: examples/canvas.md
</patterns>
<red_flags>
Breaks at runtime:
URL.revokeObjectURL() — every selection pins another image in memory for the life of thepage — revoke in the effect cleanup and before replacing the URL
URL.createObjectURL() called during render — a new URL per render, none of them revoked — moveit into an effect
clamp before assigning canvas.width
<img> — the browser rotates it asecond time — set image-orientation: none on that element, or do not normalise for display
black — fillRect white first
Surprising behaviour:
toBlob() is asynchronous and toDataURL() is not, and the synchronous one is the expensive onestring on the main thread
image-orientation, so server-side canvas work still needs manual EXIF handlingcreateImageBitmap skips layout and decode, and its result holds memory until close()toBlob() is not available across browsers; decoding it is</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.