为什么
.NET 读一张图,不该需要原生库
今天在 .NET 里把一个文件变成像素,大致有三条路:
- System.DrawingGDI+,只能在 Windows 上用
- SkiaSharp · OpenCvSharp · Magick.NET每个平台各带一份原生库,体积大,AOT / WASM 都要另想办法
- ImageSharp纯托管,但 JPEG 解码用时是 libjpeg-turbo 的 1.77 倍
SimdCodecs 是第四条:每个编解码器都用 C# 从规范写起,AVX2 / SSE / AdvSimd 手写内核,标量回退兜底。目标不是“能用”,而是每个编码器和解码器都达到或超过对应的原生参考库。
三个库吃同一块像素内存:解码直接写进缓冲区,原地增强,直接识别。以前第一步要借 ImageSharp 或 SkiaSharp,现在这一步也是纯 C# 了。
格式
一个包,七种格式
门面 Sdcb.SimdCodecs 按魔数识别格式并分派;每种格式也是独立的包,有更丰富的 API。标着“浏览器打不开”的,可以去实验台亲眼看看。
JPEG
解码编码基线 / 渐进、任意子采样、CMYK / YCCK、1/2–1/8 缩放解码、零分配 DecodeTo;编码支持优化 Huffman、渐进、EXIF / XMP
Ultra HDR
解码编码JPEG 增益图读取、组装与 HDR 重建(Ultra HDR / Adobe)
PNG
解码编码1–16 位全部位深与调色板、Adam7 隔行、文本块、HDR 信令(cICP / mDCV / cLLI)
WebP
解码编码有损、无损、Alpha、动画与按时间戳跳转;与 libwebp 逐字节一致
GIF
解码编码87a / 89a、隔行、透明、全部 disposal;编码静图与动画(精确调色板或 Wu 量化 + 抖动)
EXIF
解码编码从 JPEG、TIFF、PNG 读取;ExifWriter 为 JPEG 编码器生成数据块
ICC
解码编码解码器返回内嵌色彩配置;自带 sRGB v4;可把配置嵌入现有 JPEG
上手
解码进你自己的内存,任意布局、任意 stride
using Sdcb.SimdCodecs;
using Sdcb.SimdCodecs.Abstractions;
byte[] file = File.ReadAllBytes("photo.webp"); // JPEG, PNG, TIFF, WebP, GIF, JPEG 2000, JBIG2
// 按魔数识别格式,只读文件头
if (!SimdCodecs.TryReadInfo(file, out ImageInfo info)) throw new InvalidDataException();
Console.WriteLine($"{info.Width}×{info.Height}, {info.BitDepth}-bit, {info.FrameCount} frame(s), {info.Orientation}");
// 解码到你自己的缓冲区(R G B A)
byte[] rgba = new byte[info.Width * info.Height * 4];
SimdCodecs.TryDecodeIntoRgba8(file, rgba);// 直接写进 SkiaSharp / WPF / OpenCV 的像素内存,中间没有 RGBA 帧
using SKBitmap bmp = new(info.Width, info.Height, SKColorType.Bgra8888, SKAlphaType.Unpremul);
unsafe
{
Span<byte> dst = new((byte*)bmp.GetPixels(), bmp.RowBytes * bmp.Height);
SimdCodecs.TryDecodeInto(file, dst, bmp.RowBytes, ImagePixelFormat.Bgra32);
}
// OCR 只要灰度:彩色 JPEG 直接取 Y 平面,色度整个跳过
byte[] grey = new byte[info.Width * info.Height];
SimdCodecs.TryDecodeInto(file, grey, info.Width, ImagePixelFormat.Gray8);using Sdcb.SimdCodecs.Jpeg;
// SIMD 编码器:基线 / 优化 Huffman / 渐进,4:2:0 或 4:4:4,可带 EXIF 与 XMP
byte[] jpeg = JpegEncoder.Encode(rgba, info.Width, info.Height, channels: 4, new JpegEncodeOptions
{
Quality = 90,
Subsampling = JpegSubsampling.Chroma420,
Progressive = true,
});
File.WriteAllBytes("photo.jpg", jpeg);// 动图 WebP / GIF 的合成帧(带时长)、多页 TIFF 的每一页,同一套 API
if (SimdCodecs.TryOpenFrames(file, out IImageFrameReader? reader))
using (reader)
{
for (int i = 0; i < reader.FrameCount; i++)
{
reader.TryGetFrameInfo(i, out ImageFrameInfo frame);
byte[] buffer = new byte[frame.Info.Width * frame.Info.Height * 4];
reader.TryDecodeFrameInto(i, buffer, stride: 0, ImagePixelFormat.Rgba32);
Console.WriteLine($"#{i}: {frame.Info.Width}×{frame.Info.Height}, {frame.Duration?.TotalMilliseconds} ms");
}
}每个包都带可直接运行的 LINQPad 样例(在 LINQPad 的 NuGet 浏览器里就能看到),覆盖 SkiaSharp、OpenCvSharp、Ultra HDR、多页 TIFF 等。
性能
对手是原生库本身
单线程,AMD Ryzen 7 5800X,.NET 10。用时相对原生库,越短越快。
JPEG 解码到 RGBA · 446 张
每次解码 0 B 托管分配(ImageSharp 约 16 KiB 外加它自己的图像对象)。精度至少与 libjpeg-turbo 默认相当(色度上采样处 RGB PSNR 高约 1 dB),在 AVX2、SSE、AdvSimd、标量与 NativeAOT 上逐位一致。完整报告
WebP 解码到 RGBA · 1920×1080
输出与 libwebp 逐字节一致。照片类不透明有损文件上两者持平(0.87–1.13×)。完整报告
实验台
同一套 C#,在你的浏览器里
编译成 WebAssembly,图片不离开你的电脑。只做浏览器自己做不到的三件事:
NuGet
13 个包,同一个版本号
Sdcb.SimdCodecs (facade)
├─ Abstractions
├─ Png ──────────┬─ Color.Icc ─ Abstractions
│ └─ Abstractions
├─ Tiff ─────────┬─ Png
│ └─ Abstractions
├─ Jpeg ─────────── Abstractions
├─ Jpeg.GainMap ─┬─ Jpeg
│ └─ Abstractions
├─ Jbig2 ────────── Abstractions
├─ Jpeg2000 ─────── Abstractions
├─ Webp ─────────── Abstractions
└─ Gif ──────────── Abstractions
Exif ─ Tiff Jpeg.IccInjector (no dependencies)所有包同步发布、版本一致。包之间从不使用彼此的 internal:两个包要共用的代码以源码形式编进各自的程序集。
洁净室
只从规范写,只拿原生库当黑盒
- 算法只来自 ITU-T / ISO 规范(T.81、T.88、T.800…)、PNG 规范、RFC 6386 / 9649 的正文与表格、论文与 CPU 手册
- 从不阅读 libjpeg-turbo、libpng、libwebp、ImageSharp 或任何 GPL 代码,每个文件都是一手 MIT
- 原生库只作为黑盒参照:CI 里逐张对比输出(REQUIRE_ORACLES=1)
- 对恶意输入加固:JBIG2 段头、EXIF IFD、循环的 TIFF IFD 链
致谢
派生自 SharpAstro/Codecs(Unlicense);JPEG 的标记与熵编码层移植自公有领域的 stb_image。
已知的限制按编解码器逐条列在 docs/known-gaps.md。
MIT 许可,可放心商用。

