
上面是一张 3072×4096 的手机 HEIC 照片,Chrome 打不开。左边的画面是 Sdcb.SimdCodecs 2.0 编译成 WebAssembly 后在浏览器里解出来的。右边的「解码选项」里,我选了 1/4 缩放,所以解码输出直接就是 768×1024,中途不会先生成一张 1200 万像素的整图。
SimdCodecs 2.0.0 已经在 11 月 9 日发布了。这一版把公共 API 重新设计了一遍,所有格式现在都能做同样的事:解码到任意像素布局,缩放、裁剪,按 EXIF 方向转正,带着元数据出来;编码也只有一套入口。
bash
dotnet add package Sdcb.SimdCodecs
csharp
using PixelBuffer image = ImageCodecs.Decode(file, PixelFormat.Rgba32, new DecodeOptions { ApplyOrientation = true });
ImageCodecs.Encode(image.Pixels, image.Width, image.Height, image.Format, ImageFormat.Jpeg, output,
stride: image.Stride, options: new JpegEncodeOptions { Quality = 90 }, metadata: image.Metadata);
读一张图,摆正,带着 ICC、Exif、XMP 存成 JPEG,就这两行。
1.4 之后又做了什么
上一篇 文末的「2.0 展望」已经介绍过 PixelFormat、DecodeOptions、元数据、ImageDecoder.Open 和统一编码入口,这里不再重复。从 v1.4.0 到 v2.0.0 一共 137 个提交,新增约 4.6 万行。展望里没提到、之后才合进来的有这些:
| 日期 | 新增 |
|---|---|
| 11-05 | APNG 解码和编码;TIFF 补全:BigTIFF、调色板 / CMYK / YCbCr / alpha、JPEG 压缩、按条带读区域;JPEG 2000 多 tile 并行解码 |
| 11-06 | JPEG 编码支持 restart 间隔;从 ICC 识别 Display P3 / BT.2020 色域;HEIF 的 Exif 方向重置 |
| 11-08 | PNG 有损调色板量化(pngquant 那种用法);所有格式的缩放和区域解码 |
| 11-09 | ApplyOrientation 自动转正;解码失败抛出带原因的异常;JBIG2 多页;JPEG 的 Extended XMP;编码选项类型检查 |

先说 breaking changes
大版本号,就是因为有不兼容的改动。我先把它们列清楚。
门面类 SimdCodecs 改名成了 ImageCodecs。 这是我设计上的失误:类名和命名空间段同名,只要你项目的命名空间里也带 SimdCodecs,编译器就会把 SimdCodecs.TryReadInfo 当成你自己的命名空间去找,报 CS0234。我用 1.4.0 写了一个最小复现,命名空间叫 MyApp.SimdCodecs,再换成 2.0.0 和 ImageCodecs 对比一下:

这种问题只能靠改名解决,所以 2.0 没有保留旧名字的别名。

其余的不兼容改动:
ImagePixelFormat枚举换成了PixelFormat结构,PixelFormat.Rgba32、.Bgra32、.Rgb24、.Bgr24、.Gray8还是原来的布局;- 门面上的
TryDecodeIntoRgba8删掉了,改用TryDecodeInto(data, buffer, 0, PixelFormat.Rgba32),或者直接ImageCodecs.Decode(data, PixelFormat.Rgba32); IDecodedImage现在是IDisposable,Dispose 之后再读Pixels会抛异常;- WebP 不传编码选项时,默认是有损 ;要无损,传
new WebpLosslessEncodeOptions(); - 给编码器传了别的格式的选项(比如把
JpegEncodeOptions传给 WebP),以前会悄悄用默认值,现在直接抛ArgumentException; - HEIF Main 10 解码出来是 16 位样本,不再是 8 位。
大多数代码改个名字就行。所有 8 位的解码输出和 1.x 逐字节一致 ,HEIF Main 10 除外。完整清单见 迁移指南。
所有格式都能缩放、裁剪、转正
1.x 里只有 JPEG 能缩放解码。2.0 里,DecodeOptions.ScaleDenominator 对每个格式都接受任意 2 的幂:
- JPEG 在 DCT 域缩小(1/2 到 1/8),JPEG 2000 直接取对应的分辨率级;
- PNG(非隔行)、有损 WebP 和 HEIF 边解码边缩放,不会生成整张图;
- 其余格式先完整解码,再用一个固定的 box filter 缩小(在 gamma 空间、按 alpha 加权)。
不管走哪条路,传一个合法的 2 的幂都不会抛异常。TryGetClosestSize 会告诉你哪些尺寸是原生支持的。
HEIF 的边解边缩,是每个 tile 解完就直接缩小写进输出。和 1.x 里「先解整张、再缩小」相比,3072×4096 的手机照片缩到 1/2,8 核上耗时从 111 ms 降到 76 ms,峰值内存增量从 72 MB 降到 38 MB(记录)。
Region 是裁剪:TIFF 和 HEIF 只读和区域相交的条带或 tile;JPEG、PNG、WebP 只转换区域里的行,PNG 和 WebP 解到区域的最后一行就停。所以一个尾部损坏的文件,整张解码失败,取上半部分的区域却可能成功。
ApplyOrientation = true 会按 EXIF / TIFF 的方向把图转正,区域也按转正后的坐标算,输出元数据里的 Orientation 会重置成 1,下游不会再转一次。手机竖着拍的照片,以前要自己写旋转,现在一个选项就行。
实验台里也能试。下面是一张 1600×1067 的相机 JPEG,选 1/4 缩放,输出 400×267,wasm 里解码只用了 5.5 ms:


从 ImageSharp 迁移
第一篇我说过,做 SimdCodecs 的起因之一,就是我的开源库没法依赖需要 license key 的 ImageSharp 新版本。2.0 的 API 有一部分就是照着「从 ImageSharp 换过来要顺手」去设计的,README 里有一张完整的对照表,项目主页上也放了常用的几行:

两个行为差异要注意:
- 不设
ApplyOrientation时,像素按文件里存的方向输出,不会自动转正; - 16 位或 HDR 的图转成 8 位
PixelFormat是普通转换,不做色调映射。浮点、PQ / HLG 的图会直接拒绝 8 位布局,不去猜。
边界也要说清楚:SimdCodecs 只做编解码,不做缩放、画图、滤镜。需要这些,可以解码进 SKBitmap 或 OpenCV 的 Mat(仓库里有 SkiaSharp 和 OpenCvSharp 的 LINQPad 示例),也可以继续用 ImageSharp 做处理,只把读写换掉。
PNG 有损量化,和 pngquant.sdcb.ai
2.0 的 PNG 编码器多了一个有损调色板模式,PngWriteOptions.Quantize,做的就是 pngquant 做的事:把图压到最多 256 色,半透明也保留。
csharp
using IDecodedImage image = ImageCodecs.Decode(File.ReadAllBytes("photo.png"));
ImageCodecs.Encode(image, ImageFormat.Png, stream, new PngWriteOptions { Quantize = new() });
调色板在 OKLab 色彩空间里选,抖动会避开边缘。和 pngquant 比(它只当黑盒二进制用,不读源码),按同一个质量参数各自写出文件,结果 是:
| 同 SSIM 下的体积(BD-rate) | 耗时 | |
|---|---|---|
| 不抖动 | −8.7% | 0.28 倍 |
| 抖动 | −15.4% | 0.34 倍 |
pngquant 的耗时包含了进程启动和 PNG 读写,所以耗时这一列只能看个大概;体积这一列是两边自己写出的文件,比较公平。
为了自己用着方便,我顺手做了一个在线工具 pngquant.sdcb.ai。它用的就是 Sdcb.SimdCodecs.Png,编译成 WebAssembly 在浏览器里跑,图片不上传。默认参数对齐 pngquant --quality=0-20 --nofs,左边调参数时会实时显示等价的 pngquant 命令。下面这张 522×696 的截图,444 KB 压到 64.7 KB,只用了 59 色:

这个工具也是开源的:sdcb/pngquant-web。
修复与验证
2.0 修了一个 1.0 就有的 bug:arm64 上部分无损 WebP 解码出错。VP8L 的 select 预测器要把四个通道的距离加起来,AdvSimd 版本用一个字节存这个和,超过 255 就回绕了,于是在 Apple Silicon、Android、ARM 服务器上,一部分无损 WebP 会解出错误的像素。x64 不受影响。原因是之前 CI 的 SIMD 分档测试没覆盖 WebP。现在 WebP 和 HEIF 的测试在 Vector256、Vector128、标量和 arm64 每一档上都跑。
另外,issue #1 里报告的几个解不开的 JBIG2 和 JPEG 2000 文件,现在和 jbig2dec、OpenJPEG 的输出一致了。
发布前的验证:
- 7728 个测试,要求所有对拍用的参照程序都在场(
REQUIRE_ORACLES=1); - NativeAOT 和 Mono AOT(浏览器 wasm)下,每个门面入口的输出都和 JIT 一致;
- 性能上,和 1.x 同一次运行交替测量,整图解码路径都在噪声范围内。
还有一个数字要更正:第一篇里我写 PNG 照片类解码耗时是 libpng 的 0.61 倍,那是整个测试矩阵的几何平均,里面 1 / 2 / 4 位灰度和调色板图把平均值拉低了。这次对 libpng 1.6.39 重新测,README 改成只引用最常见的 8 位 RGB / RGBA 照片,是 0.88 倍 ,ImageSharp 在同样的图上是 0.91 倍(记录)。这一项上我们和 ImageSharp 差不多。
新的 sdcb.ai/simdcodecs
项目主页 跟着 2.0 改了:代码示例换成 2.0 的 API,格式和性能改成和 README 一致的两张表,加上了 ImageSharp 迁移表。

实验台 也换成了 2.0:打开文件时可以选缩放、勾选按 EXIF 转正,开头那张图就是这么来的;格式擂台里多了一项「PNG 量化」,同一张图过 10 个编码器:

1.4 里的「小工具」页签(GIF 转 WebP、favicon 生成)这次拿掉了,图片压缩现在有 pngquant.sdcb.ai。
还没做的
最大的缺口还是格式:AVIF 和 JPEG XL 目前只能识别,不能解码 。ImageCodecs.DetectFormat 认得出它们,IsDecodable 会返回 false;AVIF 里的 Exif 倒是已经能用 ExifReader 读出来,但图像本身还解不了。
这两个是我接下来最想啃的:
- AVIF:基于 AV1,免版税,规范公开,最适合按 clean room 的规矩从规范写起。现在 Chrome、Firefox、Safari 都能直接显示 AVIF,不少网站和 CDN 已经在用,.NET 里想读它,还是只能靠 libavif、libdav1d 这些原生库;
- JPEG XL:同样免版税,但 ISO 规范要花钱买。它能把现有的 JPEG 无损地重新压缩、还能原样还原回去,这一点对存量图片很有吸引力。
和 HEIF 一样,这两个格式的解码器,每一个的工作量都不比 JPEG、PNG、WebP 加起来小,具体什么时候能出来,我现在不敢打包票。HEIF 自己也还有没做完的:图像序列(.heics),以及 Main 12、4:4:4 这些扩展 profile。
API 和性能上,2.x 还要做的,known-gaps 里都写着:
- 真正的流式输入:传
Stream进来,目前还是先整个读进内存; - 按行流式编码;
- JPEG 的区域解码:现在每个 MCU 仍要熵解码,耗时接近整张解码,按 restart 间隔跳过要等 2.x;
- JPEG 2000 单核耗时仍是 libopenjp2 的约 2 倍,多核下是 1.15 到 1.75 倍。
最后
- GitHub:sdcb/SimdCodecs,v2.0.0 发布说明
- NuGet:
dotnet add package Sdcb.SimdCodecs,HEIC 另装Sdcb.SimdCodecs.Heif - 迁移指南:docs/migrating-to-2.0.md
- 项目主页:sdcb.ai/simdcodecs,在线实验台:sdcb.ai/simdcodecs/playground
- PNG 压缩:pngquant.sdcb.ai(源码 sdcb/pngquant-web)
API 设计上有不顺手的地方,欢迎在 GitHub 上提 issue,2.x 里能不破坏兼容就改。

交流
感谢阅读到这里,如果觉得 SimdCodecs 有用,欢迎去 GitHub 点个 Star ,也欢迎评论 和点赞,这是我持续创作的动力!

欢迎关注微信公众号 .NET骚操作 ,或者扫码加入 .NET骚操作 微信交流群:

(如果微信群二维码过期,请加入 .NET骚操作交流QQ群 :495782587 ;做图像和视觉相关开发的朋友,也欢迎加入 C#/.NET计算机视觉技术交流 QQ 群 :579060605)