关于 web-files-image-handling
web-files-image-handling 是一项关于浏览器图片预览与 Canvas 处理的指南技能,面向 Web 开发者。它分别提供显示和转换的做法,帮助避免方向、内存及画质问题。
主要功能
- 01创建并清理对象 URL 预览
- 02使用 Canvas 调整尺寸和压缩
- 03处理 EXIF 方向
- 04多次缩小图片
- 05裁剪与格式转换
适用场景
- 01在 Web 应用中预览所选图片
- 02上传前调整或压缩图片
- 03生成缩略图或裁剪图片
- 04规范化导出图片数据的方向
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-files-image-handling安装后在 Claude Code 中输入/web-files-image-handling确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/canvas.mdcore.mdpreview.md详细介绍
图像处理模式
快速指南: 两项不同的任务,需要使用两种不同的工具。显示图像时,使用
URL.createObjectURL(),之后再撤销对象 URL——浏览器已经会应用 EXIF 旋转,因此 手动旋转会导致旋转两次。处理图像时,使用 Canvas:将尺寸限制在 4096px,缩小幅度超过 50% 时分多次缩放,写入 JPEG 前先填充白色,并优先使用异步的toBlob(),而不是toDataURL()。
详细资源:
- examples/core.md — 共享的
loadImage辅助函数、预览 hook 和组件、尺寸提取与验证、EXIF 解析、图库状态 - examples/preview.md — 并行生成缩略图组、图库网格
- examples/canvas.md — 缩放流程、目标大小压缩、裁剪、水印、滤镜
- reference.md — 格式与策略选择、常量、方法对比、EXIF 值、浏览器支持
适用路径
这两种路径对 EXIF 的处理方式不同,而大多数 bug 都出在这里。
- 向用户显示图像 — 将对象 URL 直接传给
<img>,替换图像或卸载组件时撤销 URL。浏览器会自动旋转;无需处理方向。请从 examples/core.md 开始。 - 处理图像后再发送到其他地方 — 使用 Canvas 缩放、压缩、转换或裁剪。这里不会自动旋转,因此要显式规范方向,并且显示结果时务必禁止浏览器自行旋转。请从 examples/canvas.md 开始。
<critical_requirements>
编写图像处理代码前
撤销每个对象 URL——卸载时,以及创建替代对象 URL 之前。 每个
createObjectURL 都会将其 blob 固定在内存中,直到被撤销;因此,如果用户改变主意,更换
选取的文件,每次尝试都会泄漏一张完整图像。
处理方向前,先确定你采用哪种路径。 自 2020 年起,浏览器默认使用
image-orientation: from-image,因此为显示而规范方向会导致图像旋转两次;
只对要离开浏览器的字节数据进行方向规范化。
将 canvas 尺寸限制在 4096px。 超过该尺寸时,canvas 可能静默失败或导致标签页崩溃, 而移动设备上的限制还要更低。
缩小幅度超过一半时,分多次缩放。 单次降采样会造成采样不足,产生模糊且有锯齿的结果; 通过两个中间步骤可以保持清晰。
</critical_requirements>
自动检测: URL.createObjectURL、URL.revokeObjectURL、createImageBitmap、OffscreenCanvas、 canvas.toBlob、canvas.toDataURL、convertToBlob、ctx.drawImage、imageSmoothingQuality、 image-orientation、EXIF orientation、0x0112、FileReader.readAsDataURL、naturalWidth、image/webp、 逐步缩小、缩略图生成、客户端调整大小、图像裁剪
适用于:
- 显示用户选择的图像预览
- 在浏览器中调整图像大小、压缩或转换图像
- 读取并规范化 EXIF 方向
- 生成缩略图、裁剪、添加水印、应用滤镜
- 在进行其他操作前验证尺寸和宽高比
其他内容另行处理:
- 选择和传输文件——此技能从你已持有的
File或Blob开始 - 在服务上调整大小或转码,以及基于 URL 的转换服务
- 带有图层、选区和撤销功能的交互式编辑界面;这类场景需要使用 canvas 库,而不是直接调用 Canvas
## 理念
预览和处理的目标相反,将两者混为一谈是大多数图像错误的根源。
预览不应产生任何开销:URL.createObjectURL() 会将内存中已有字节的引用交给 <img>,无需解码到 JavaScript,也无需复制。代价是需要手动管理生命周期——如果不撤销引用,它就会一直存在,超出组件的生命周期。
处理则恰恰相反:必须将像素解码到画布中、进行转换,再重新编码。所有耗时操作都发生在这里,因此值得只在需要的尺寸上进行一次画布处理,而不是反复以全分辨率处理。
浏览器自身的 EXIF 处理横跨两者之间的界线。浏览器会在显示时旋转图像,但不会在画布中旋转,因此同一个文件经由不同路径读取时,方向会有所不同。
核心模式
模式 1:使用对象 URL 预览并清理
创建下一个 URL 之前,先撤销上一个 URL,并在卸载时撤销 URL。在渲染期间创建 URL,会导致每次渲染都泄漏一个 URL。
useEffect(() => {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
return () => URL.revokeObjectURL(url);
}, [file]);
完整代码:examples/core.md
模式 2:画布缩放
将尺寸限制在浏览器上限内,保持宽高比,启用高质量平滑;输出格式为 JPEG 时,绘制前先填充白色——JPEG 没有 alpha 通道,因此透明区域会编码为黑色。
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);
完整代码:examples/canvas.md
模式 3:逐步缩小
drawImage 会对固定邻域进行采样,因此一次大幅缩小会丢弃大部分源图像信息。反复减半可以让每个像素都参与其中。
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 */
}
}
完整代码:examples/canvas.md
模式 4:EXIF 方向
// 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" }} />
完整代码:examples/core.md
模式 5:格式转换
不同格式对质量参数的含义不同,而 PNG 会完全忽略该参数。
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 编码,包括 Safari 14 及更高版本。若要满足字节数预算,而非指定质量设置,可以对质量参数进行二分搜索。
完整代码:examples/canvas.md
模式 6:裁剪
drawImage 使用九个参数时,会同时接受源矩形和目标矩形,因此一次调用即可完成裁剪和缩放。先将源矩形限制在图像范围内——超出边界的区域不会绘制任何内容,而不会报错。
// drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh)
ctx.drawImage(
img,
cropX,
cropY,
cropWidth,
cropHeight,
0,
0,
outputWidth,
outputHeight,
);
完整代码:examples/canvas.md
<red_flags>
风险提示
运行时会出问题:
- 没有
URL.revokeObjectURL()— 页面存续期间,每次选择都会让另一张图片占用内存 — 在 effect 清理时以及替换 URL 之前撤销 URL - 在渲染期间调用
URL.createObjectURL()— 每次渲染都会创建一个新 URL,且没有一个会被撤销 — 将其移入 effect - canvas 尺寸超过 4096px — 标签页会崩溃,或 canvas 会静默地什么也不输出 — 赋值给
canvas.width之前先限制尺寸 - 先规范化方向,再在
<img>中渲染结果 — 浏览器会再次旋转图片 — 在该元素上设置image-orientation: none,或显示时不要规范化方向 - 将透明 PNG 转换为 JPEG 时没有填充背景 — 透明区域会变成黑色 — 先用
fillRect填充白色
容易令人意外的行为:
- 单次缩小超过 50% 会显得模糊且有锯齿,而两次缩小则显得清晰
toBlob()是异步的,而toDataURL()不是;同步的那个开销更大- data URL 大约比它编码的文件大三分之一,因此 10MB 的图片会变成主线程中的 13MB 字符串
- Node 不支持
image-orientation,因此服务端 canvas 操作仍需手动处理 EXIF createImageBitmap会跳过布局和解码,其结果会一直占用内存,直到调用close()- 图片的每个维度都在限制范围内,仍可能超出 canvas 的总面积限制
- 各浏览器并不都支持通过
toBlob()编码 AVIF;解码则支持 - 重新编码已经优化过的图片通常会让文件变大,而不是变小