使用方法
- 点「选择 WebP 文件」挑一个本地
.webp(上限 20 MB,即 20971520 字节;超限会直接拒绝并说明,不会进内存)。 - 点「开始转换」。页面会先读文件头 12 字节核对 RIFF 魔数,再解码——不是先分配画布再看尺寸,这样一张 8000×6000 的压缩包不会先把标签页撑爆。
- 结果区给出 7 个值:输出尺寸、源体积、PNG 体积、放大倍数、像素总数与中间缓冲字节数、以及实际走了哪条解码路径。
- 想直接看图就点「下载 PNG」,或看预览区——画布默认透明,无 alpha 的区域会显示为页面底色。
- 下方 18 行对照表是「不同尺寸下 WebP 转 PNG 需要多少中间缓冲」的固定参考,服务端直出,脚本不执行也能看到完整表格;点「导出 CSV」拿走实测向量(带 BOM)。
这个 WebP转PNG转换器能解决什么问题
一是网站不允许 WebP 的场合。微信内置浏览器、邮件附件、部分政府与银行网银、以及 background-image 里被旧 IE 兼容代码覆盖到的路径,遇到 WebP 会直接不显示。这类场景必须转成 PNG 或 JPEG,而WebP转JPG 会丢透明,所以带透明通道的图标与截图只能走本页。
二是PNG 不支持动画。WebP 动图转成 PNG 后只保留第一帧(createImageBitmap 对动图就是取首帧),这不是 bug 而是格式本身没有帧序列概念。需要保留动画请直接用 WebP,或改用 APNG(但 canvas.toBlob 同样不支持输出 APNG)。
三是拿到一份能审计的原始像素。WebP 走了有损量化,转成 PNG 之后你拿到的是「解码后重新编码的像素」,不是原始数据。如果你是在做取证或像素级比对,必须清楚这一点:转换过程不做任何增强,只做 drawImage 的 1:1 拷贝。
四是给 canvas 或 toDataURL 准备素材。很多前端库只接受 PNG/JPEG 的 dataURL,而 WebP 源文件在 <img> 里能显示、在 canvas 上下文上却可能被浏览器拒收(取决于版本),先转一遍最省事。
本页不做这些事:不压缩(PNG 是无损格式,输出只会比源大,压缩请用图片压缩工具)、不缩放尺寸(要改尺寸用图片尺寸调整)、不认识矢量图(SVG 走SVG转PNG,那是完全不同的渲染路径)。
原理和变量
整个流程是三步,每一步都有可替代的 API,替代是有代价的:
第一步,魔数预检。WebP 是 RIFF 容器:文件头 4 字节必须是 RIFF(0x52 0x49 0x46 0x46),第 9~12 字节必须是 WEBP;紧接着的 chunk FourCC 区分三种格式——有损 VP8 (含尾空格)、无损 VP8L(首字节是 0x2F 签名,之后用 14 比特存宽-1、14 比特存高-1、1 比特 alpha 标志、3 比特版本)、扩展容器 VP8X(1 字节 flags + 3 字节保留 + 3 字节 canvas 宽-1 + 3 字节 canvas 高-1)。注意 fileSize 字段存的是实际字节数减 8。本页用 file.slice(0, 12).arrayBuffer() 读这 12 字节做校验,代价可忽略。
第二步,双层解码。第 1 层 createImageBitmap(file) 走浏览器原生解码路径,能拿到 ImageBitmap.width/height 而不必先分配画布;第 2 层是 new Image() 配 URL.createObjectURL(file),全平台兜底,但必须调用 revokeObjectURL,否则 blob URL 一直占着内存不释放。什么情况下会落到第 2 层?旧版 Safari 没有 createImageBitmap;某些动图 createImageBitmap 抛错。两层都失败的典型输入是「把文本文件改名成 .webp」或「被截断的半张图」——所以失败文案必须区分「这不是 WebP 文件」和「这是 WebP 但文件损坏」。
第三步,canvas 编码。ctx.drawImage(bitmap, 0, 0) 把位图搬进画布,然后 canvas.toBlob(cb, 'image/png')。这里有三条规范级事实:
- type 参数只认两种值。HTML 规范对
HTMLCanvasElement.toBlob(callback, type, quality)只定义了image/png与image/jpeg。传image/webp浏览器会忽略并回落成image/png——blob.type会如实变成image/png,不会报错。所以本页界面上的 quality 滑块是留着给你看「它无效」的:PNG 是无损格式,quality 被完全忽略,传 0.1 与传 1.0 得到的字节数逐位相同。这是规范规定,不是浏览器 bug。 - toBlob 允许返回 null。内存不足时规范允许回调收到
null,所以必须判空并给出中文文案,不能把null当空文件静默处理。 - 回调是异步的。必须在回调里用
blob.arrayBuffer()取字节,不能在回调外同步读。
为什么 WebP 转 PNG 通常会变大?PNG 的体积结构是「8 字节签名 + 25 字节 IHDR + IDAT + 12 字节 IEND」,除 IDAT 外恒为 45 字节,占比可忽略,体积几乎完全由 zlib 压缩后的像素流决定。而 WebP 的 VP8 内部分块变换加有损量化已经把熵压得很低,PNG 的 zlib deflate 无法再从同一份数据上压出同等密度。想真正压小 PNG,只能改 color type:本页输出的画布是 RGBA(color type 6,每像素 4 字节),若原图确无 alpha 可降到 type 2(3 字节)省 1/4 原始体积,若色彩数 ≤ 256 还能降到 type 3 加 PLTE 索引色省得更多——但这两条都要求改动像素数据本身,不属于「转换」的范畴。
完整示例
用 Node + zlib 造一张可复现的 64×64 RGBA 渐变图做标定(数值仅作演示):未压缩原始像素流 = 16448 字节(每行 1 字节 filter + 64×4 字节像素),zlib.deflate level 6 后 IDAT = 11810 字节,加上 8 + 25 + 12 = 45 字节固定开销,PNG 总长 11867 字节,其中 IDAT 占 99.5 %。这组数字说明:PNG 的体积几乎就是 IDAT 的体积,其余部分可以忽略。
同样尺寸、同样的 zlib level,把色深从 type 6 降到 type 2(去掉那 4096 个全 255 的 alpha 字节)后是 10167 字节,省 1700 字节。压缩前的原始差正好是 4096 字节 = 64×64 个 alpha 字节。这解释了为什么「源 WebP 本来没 alpha,转出来的 PNG 却还是 type 6」——canvas 2D 上下文一律按 4 字节/像素存。
所以「WebP 转 PNG 后体积放大多少倍」这个问题没有固定答案,取决于源图的内容与压缩参数。页面里那列「体积放大倍数」永远是对你自己这张图实测出来的 PNG 字节 / 源字节,本页不预置任何假数据。
输入范围与对照表
三道阈值,按先到者生效:文件体积 ≤ 20 MB(20971520 字节)、边长 ≤ 12000 px、面积 ≤ 40000000 px。为什么要有面积上限?画布 RGBA 中间缓冲是 宽 × 高 × 4 字节,与源是否带 alpha 无关:1000×1000 = 3.8 MiB,4000×3000 = 45.8 MiB,8000×6000 = 183.1 MiB——移动端标签页会被系统直接杀掉。所以 8000×6000 以上的图本页会在分配画布之前就拒绝。
文件选择器写 accept="image/webp,.webp",MIME 与后缀都给,是为了覆盖不认 .webp 后缀的旧浏览器。
| 输出尺寸 (px) | 像素总数 | RGBA 中间缓冲 | 本工具是否受理 | 说明 |
|---|---|---|---|---|
| 320×240 | 76,800 | 0.3 MiB | 受理 | 在阈值内,正常转换 |
| 640×480 | 307,200 | 1.2 MiB | 受理 | 在阈值内,正常转换 |
| 800×600 | 480,000 | 1.8 MiB | 受理 | 在阈值内,正常转换 |
| 1024×768 | 786,432 | 3.0 MiB | 受理 | 在阈值内,正常转换 |
| 1280×720 | 921,600 | 3.5 MiB | 受理 | 在阈值内,正常转换 |
| 1440×900 | 1,296,000 | 4.9 MiB | 受理 | 在阈值内,正常转换 |
| 1920×1080 | 2,073,600 | 7.9 MiB | 受理 | 在阈值内,正常转换 |
| 2048×1536 | 3,145,728 | 12.0 MiB | 受理 | 在阈值内,正常转换 |
| 2560×1440 | 3,686,400 | 14.1 MiB | 受理 | 在阈值内,正常转换 |
| 3000×2000 | 6,000,000 | 22.9 MiB | 受理 | 在阈值内,正常转换 |
| 3840×2160 | 8,294,400 | 31.6 MiB | 受理 | 在阈值内,正常转换 |
| 4000×3000 | 12,000,000 | 45.8 MiB | 受理 | 在阈值内,正常转换 |
| 5000×4000 | 20,000,000 | 76.3 MiB | 受理 | 在阈值内,正常转换 |
| 6000×4000 | 24,000,000 | 91.6 MiB | 受理 | 在阈值内,正常转换 |
| 8000×6000 | 48,000,000 | 183.1 MiB | 拒绝 | 面积超 4000 万,先缩尺寸 |
| 12000×9000 | 108,000,000 | 412.0 MiB | 拒绝 | 面积超 4000 万,先缩尺寸 |
| 12000×12000 | 144,000,000 | 549.3 MiB | 拒绝 | 面积超 4000 万,先缩尺寸 |
| 40000×30000 | 1,200,000,000 | 4577.6 MiB | 拒绝 | 面积超 4000 万,先缩尺寸 |
全部 18 行服务端直出,不依赖 JS 执行;超出面积或边长阈值的行标黄并写明拒绝原因。
几个常见误用的对照:
- WebP 转 PNG 想让它变小 → 方向反了。PNG 无损,转换只会让体积上升;要压体积请走图片压缩工具。
- 转完发现没有透明背景了 → 检查源图是否真有 alpha。本页不做去白底,因为那要改像素值。
- 转完动画只剩一张 → PNG 没有帧序列概念,属格式限制。
- 转完体积暴涨 10 倍以上 → 通常是纯色插画类 WebP(VP8L 压得很好,PNG 压不动)。这类图更适合 SVG,见SVG转PNG。
边界与精度
1. 颜色类型是理解体积的关键。PNG 的 color type 共 5 种:0 灰度、2 真彩色 RGB、3 索引色(需 PLTE 调色板)、4 灰度+alpha、6 真彩色+alpha(RGBA)。每像素字节:type 2 是 3 字节,type 6 是 4 字节,RGBA 比 RGB 多 33.3 % 原始体积。canvas 2D 输出的 PNG 只会是 2 或 6。
2. alpha 通道会被保留,但有舍入。canvas 2D 上下文默认 alpha: true,drawImage 保留源的 alpha。需要注意 canvas 内部按预乘 alpha 存储,而 getImageData 读回的是非预乘值——半透明边缘像素来回转换会有 1 级舍入差。这是画布模型的固有行为,不是转换损失。
3. 尺寸上限取 12000 px 与 4 千万像素,不赌浏览器常量。HTML 规范没有给统一的画布尺寸上限,各家实现差异很大且随版本变化。本页的做法是自己设阈值并先判,这样规则可预期、可写在页面上。超限时给的是明确中文文案,而不是让浏览器抛一个看不懂的异常。
4. 动图只取首帧。createImageBitmap 对动画 WebP 返回的是首帧的位图;Image 回退路径也是一样的结果。所以本页对动图的处理是「正确但有损」——输出必然是静态 PNG,页面不会假装保留了动画。
5. 体积对照不写死倍数。同一张图用不同 WebP 质量档编码,转成 PNG 的放大倍数完全不同。所以本页的「放大倍数」列是实测值;对照表里只列尺寸与内存开销这类能从输入算出来的量,不编造放大比例。
6. CSV 是单行汇总。导出内容是「你当前这张图」的一行实测数据(尺寸、源体积、PNG 体积、倍数、像素数、解码路径),带 3 字节 BOM 供 Excel 正确识别 UTF-8;行尾用 CRLF。它不是对照表那种固定虚表。
常见问题
为什么我拖进来的是 .webp 却被拒绝,提示说这不是 WebP 文件?
因为文件头不对。WebP 必须以 RIFF 开头、第 9~12 字节是 WEBP。最常见的原因是文件被改过扩展名——比如把 .jpg 直接重命名为 .webp,扩展名变了但内容还是 JPEG,此时前 4 字节是 FF D8 FF E0,自然对不上。请确认文件是真正的 WebP,Windows 上可看文件属性里的「类型」是否写着 WebP 图片。
提示「是 WebP 但浏览器解不开」,跟「不是 WebP 文件」有什么区别?
前者说明 12 字节魔数校验通过了,文件确实是 WebP 容器,但解码失败。典型原因是文件被截断(比如下载没下完)、动图的帧数据损坏、或者浏览器对这个 WebP 变体支持不足。这种情况换一张图试试即可,文件本身需要重新获取。后者连魔数都没过,是文件类型不对,换一个正确的 WebP 就行。页面把这两条分开写,就是为了让你知道该换文件还是该重下文件。
我把 quality 从 1 调到 0.1,为什么文件一点没变小?
因为 PNG 是无损格式。canvas.toBlob(callback, 'image/png', quality) 的 quality 参数只对有损格式(JPEG、WebP)有意义,HTML 规范对 PNG 明确忽略它。所以传 0.1 和传 1.0 拿到的字节数逐位相同。这个 quality 滑块本页故意留着,就是让你当场验证这条规范——很多工具在这里悄悄把 PNG 转成 JPEG 再输出,那会引入二次量化损失并丢掉透明,是不可逆的改动。
转出来的 PNG 反而比源 WebP 大好几倍,是工具出错了吗?
不是。WebP 转 PNG 只会让体积上升,因为 PNG 无损而 WebP 常常是有损的。放大倍数取决于内容:照片类通常在 1.2 ~ 3 倍区间,纯色插画、渐变、线稿类可能到 5 ~ 10 倍(这类图 VP8L 压得非常紧,PNG 的 zlib 压不动)。要减小体积请改走图片压缩工具,或接受限制继续用 WebP。
源图本来没有透明背景,为什么转出来还是 PNG 的 RGBA?
因为 canvas 2D 上下文一律按 4 字节/像素存储,toBlob('image/png') 就会输出 color type 6。标定数据:64×64 的渐变图,type 6 是 11867 字节,降到 type 2 是 10167 字节,省 1700 字节;压缩前的原始差正好是 4096 字节 = 64×64 个 alpha 字节。本页不自作主张帮你降色深,因为那需要读回全部像素做一次颜色类型判定,属于会改变输出的额外处理。
转换会修改我的图片内容吗?
不会。流程是 drawImage(bitmap, 0, 0) 一次 1:1 拷贝,没有任何滤波、缩放、增强或降噪。本页也不提供「去白底」「自动裁剪」「按比例缩放」这类功能——它们都会真实改变像素值。要改尺寸请用图片尺寸调整,那是另一个工具的事。
为什么不提供「WebP 转 PNG 再转回 WebP」来压体积?
因为这条路在浏览器里是断的。HTML 规范只给 toBlob 定义了 image/png 和 image/jpeg,传 image/webp 会被静默忽略并回落成 PNG——也就是说拿不到 WebP 字节。而 WebP 编码器(libwebp)没有官方浏览器 API,要重新压回 WebP 只能靠自带的 JS 编码器,那是几百 KB 的额外体积,与本页「不上传、不加载外部库」的前提冲突。压缩 WebP 请用PNG转WebP转换器,那页从 PNG 出发才是正确方向。
我的图 5000×4000 为什么被拒绝了?明明没超 20 MB?
被面积阈值拦下的。5000×4000 = 2000 万像素,虽然低于 4 千万的面积上限、边长也没超 12000 px,但画布需要的 RGBA 中间缓冲是 2000 万 × 4 = 80 MB 起步,加上解码时的源位图、canvas 副本和最终 PNG 的字节缓冲,峰值内存会翻几倍。本页在分配画布之前就判面积,正是为了避免标签页当场白屏。缩小尺寸后再转即可,或者用图片尺寸调整先压到 4000×3000 以内(那档是 45.8 MiB,移动端也能扛)。