Skip to content

Releases: sdcb/SimdCodecs

v1.2.0: QOI and Netpbm

Choose a tag to compare

@sdcb sdcb released this 30 Oct 07:54

New packages

  • Sdcb.SimdCodecs.Qoi: QOI ("Quite OK Image") decoder and encoder, written from the specification. It decodes RGB and RGBA straight into a caller's buffer in any 8-bit layout, with no allocation. It encodes from RGBA, BGRA, RGB, BGR or grey, to an array or a stream.
  • Sdcb.SimdCodecs.Pnm: the Netpbm family, decode and encode.
    • P1–P6 PBM / PGM / PPM, plain (ASCII) and raw, with maxval up to 65535 (16-bit samples).
    • P7 PAM with 1–4 channels.
    • PFM, 32-bit float, both byte orders.

Facade

  • ImageFormat.Qoi now decodes; it was recognised only before.
  • New ImageFormat.Pnm and ImageFormat.Pfm, both sniffed by the facade.
  • PFM returns float samples. Use PixelConverter.StretchToRgba8 to get a viewable image.

Performance

Measured on a Ryzen 7 5800X at 1920×1080, single thread. Figures are the geometric mean of our time ÷ ImageSharp 3.1.12's, so lower is faster.

Decode Encode to a stream
QOI 0.29× 0.49×
Netpbm 0.23× 0.54×

Full results are in benchmarks/Sdcb.SimdCodecs.QoiPnm.Benchmarks/results/.

Verification

  • Exact against ImageMagick in both directions, for every variant it writes.
  • Hand-written streams cover every QOI chunk, plus Netpbm comments, odd maxvals and PFM byte orders.
  • Every SIMD tier is tested, and 300k fuzz iterations per format ran clean.
  • NativeAOT and Mono AOT (wasm) output matches JIT.

Samples

  • LINQPad qoi: QOI against PNG on size and speed.
  • LINQPad netpbm: every format, a hand-written PPM, 16-bit PGM, PFM.

Known gaps

  • Netpbm reads only the first image in a file.
  • PFM does not decode into 8-bit layouts.
  • PAM DEPTH is limited to 1–4.
  • QOI has not been benchmarked against a native implementation.
  • ImageSharp 3.1.12 reads and writes 16-bit PPM little-endian, unlike the spec and ImageMagick, so that case is not timed against it.

Details are in docs/known-gaps.md.

Sdcb.SimdCodecs 1.1.0

Choose a tag to compare

@sdcb sdcb released this 30 Oct 06:39

Sdcb.SimdCodecs 1.1.0 adds two codecs, BMP and ICO / CUR, each with a decoder and an encoder. Both are pure managed, NativeAOT- and browser-wasm-compatible, and SIMD-accelerated, with a bit-identical scalar fallback.

dotnet add package Sdcb.SimdCodecs        # the facade now decodes .bmp, .ico and .cur too
dotnet add package Sdcb.SimdCodecs.Bmp    # or just the codec you need
dotnet add package Sdcb.SimdCodecs.Ico

New: Sdcb.SimdCodecs.Bmp

  • Decode handles every DIB variant in practical use:
    • Headers: OS/2 1.x core, OS/2 2.x (16–64 bytes), Windows BITMAPINFOHEADER, and V2–V5.
    • Pixel formats: 1/2/4/8-bit palettes, and 16/24/32-bit pixels with arbitrary BITFIELDS / ALPHABITFIELDS masks.
    • Compression: RLE4 and RLE8 with deltas, OS/2 RLE24 and Huffman 1D, and embedded PNG.
    • Also: top-down rows, V5 embedded ICC profiles, OS/2 bitmap arrays, and bare DIBs.
    • DecodeTo / TryDecodeInto write into your buffer in any pixel layout and allocate nothing.
  • Encode writes 1/4/8-bit palettes, RLE4 and RLE8, 5-5-5 and 5-6-5, 24-bit and 32-bit BGRA.
    • Palettes are exact when the image has few enough colours and quantised otherwise.
    • Headers can be core, V3, V4 or V5, and V5 headers can carry an ICC profile.
    • A Stream overload writes one row at a time.
  • Where Windows Imaging Component (WIC), ImageMagick and browsers disagree, the package documents its choice in a Decisions table in its README. One example is whether the 4th byte of a 32-bit BI_RGB pixel is alpha.

New: Sdcb.SimdCodecs.Ico

  • Reads and writes Windows icons (.ico) and cursors (.cur).
    • Each entry can be PNG or a DIB with an AND mask.
    • Cursors carry hot spots.
  • The facade treats each entry as a frame (TryOpenFrames). TryDecode returns the largest entry, breaking ties by colour depth.
  • ImageFormat.Cur is new. .bmp, .ico and .cur files now decode through the facade instead of being only recognised.

Performance

Measured single-threaded on an AMD Ryzen 7 5800X with .NET 10, using 1920×1080 images:

Sdcb.SimdCodecs WIC ImageSharp 3.1.12
BMP decode, geometric mean of 22 layouts (time relative to WIC) 0.20× 1× 1.62×
BMP encode, 5-6-5 2.5 ms — 14.4 ms
BMP encode, 8-bit palette from a photo (quantised) 97 ms — 208 ms

WIC encodes only 24- and 32-bit BMP, and our encoder is more than 20× faster at both. ImageSharp is still faster at one thing: writing 24- or 32-bit output to a stream from RGBA input. It takes 0.28 ms and 0.23 ms against our 0.33 ms and 0.41 ms. From B, G, R(, A) input, we are faster (0.20 ms) or on par (0.23 ms). Full tables are in benchmarks/Sdcb.SimdCodecs.Bmp.Benchmarks/results.

Correctness

  • Output matches WIC pixel for pixel on a corpus covering every BMP variant, and on every entry of the test icons. It is also checked against Magick.NET and against independently hand-assembled streams.
  • Each vector kernel produces identical output on its AVX2, Vector128 and scalar paths. NativeAOT and Mono AOT produce the same output as JIT.
  • The decoder passed a 300,000-iteration fuzz run.

Other changes

  • PNG: TryReadInfo / TryDecode used to throw on an IHDR whose bit depth is not allowed for its colour type. They now return false, as the Try* contract requires. The BMP fuzzer found this through embedded PNG.
  • GIF: The palette quantizer is now shared with BMP. Its palette lookup is faster on large images, and the output is byte-for-byte unchanged.

Packages

Sdcb.SimdCodecs (facade), .Abstractions, .Jpeg, .Jpeg.GainMap, .Jpeg.IccInjector, .Png, .Tiff, .Webp, .Gif, .Bmp, .Ico, .Jpeg2000, .Jbig2, .Exif, .Color.Icc. All 15 packages are on NuGet under the MIT licence.

Full changelog: v1.0.1...v1.1.0

Sdcb.SimdCodecs 1.0.1

Choose a tag to compare

@sdcb sdcb released this 29 Oct 10:49

The first stable release of Sdcb.SimdCodecs: pure-managed image codecs for .NET 10. There are no native dependencies, the libraries are NativeAOT- and browser-wasm-compatible, and the kernels are SIMD-accelerated (AVX2, SSE, AdvSimd, WebAssembly SIMD) with a bit-identical scalar fallback.

dotnet add package Sdcb.SimdCodecs
using Sdcb.SimdCodecs;
using Sdcb.SimdCodecs.Abstractions;

byte[] file = File.ReadAllBytes("photo.webp");   // JPEG, PNG, TIFF, WebP, GIF, JPEG 2000 or JBIG2
if (!SimdCodecs.TryReadInfo(file, out ImageInfo info)) throw new InvalidDataException("Not a supported image");
byte[] rgba = new byte[info.Width * info.Height * 4];
SimdCodecs.TryDecodeIntoRgba8(file, rgba);

Formats

Format Decode Encode Notes
JPEG ✅ ✅ Baseline and progressive, any subsampling, CMYK / YCCK, 1/2–1/8 scaled decode, zero-allocation DecodeTo; the encoder writes baseline, optimised Huffman or progressive streams with EXIF, XMP and ICC
Ultra HDR JPEG ✅ ✅ Gain-map read, assembly and HDR reconstruction
PNG ✅ ✅ Every bit depth, indexed colour, text chunks, HDR signalling (cICP, mDCV, cLLI)
TIFF ✅ ✅ Multi-page; 8/16/32-bit integer and float samples; strips or tiles; reads Deflate, LZW, PackBits and CCITT G3/G4
WebP ✅ — Lossy, lossless, alpha and animation with seeking; output is byte-exact with libwebp
GIF ✅ ✅ Stills and animations, every disposal method; quantising encoder
JPEG 2000 ✅ — Part 1 (T.800), for PDF /JPXDecode
JBIG2 ✅ — T.88, for PDF /JBIG2Decode
EXIF ✅ ✅ Read from JPEG, TIFF, PNG and WebP; ExifWriter builds the block the JPEG encoder embeds
ICC ✅ ✅ Decoders return embedded profiles; IccProfiles.Describe gives a profile's name and TryGetPrimaries its gamut; bundled sRGB v4 profile

The facade

A single Sdcb.SimdCodecs reference brings in the whole family, which ships in lockstep under one version, and exposes these entry points:

  • DetectFormat: identifies a file by its signature. It also recognises formats it does not decode (HEIF, AVIF, JPEG XL, BMP, …), and IsDecodable tells the two kinds apart.
  • TryReadInfo: reads dimensions, channels, bit depth, format, EXIF orientation and frame count from the headers alone.
  • TryDecode: returns the native layout (8/16-bit or float samples) together with the ICC profile and an H.273 colour description.
  • TryDecodeInto: writes RGBA, BGRA, RGB, BGR or grey straight into your buffer, at any row stride.
  • TryOpenFrames: decodes frame by frame, covering animated WebP and GIF frames and multi-page TIFF.
  • PixelConverter.StretchToRgba8: makes float and HDR rasters viewable, with optional sRGB encoding for linear light.

Every Try* call on untrusted input returns false instead of throwing, hanging or allocating without bound.

Performance

Measured single-threaded on an AMD Ryzen 7 5800X with .NET 10. Each time is relative to the native library, so lower is faster:

Time
JPEG decode to RGBA (446 files), vs. libjpeg-turbo 3.2.0 0.82×, with no managed allocation per decode
WebP decode, 1920×1080, vs. libwebp 1.6.0 0.88× lossy, 0.53× lossy + alpha, 0.72× lossless

The JPEG decoder is at least as accurate as libjpeg-turbo's default mode. Its output is bit-identical on AVX2, SSE, AdvSimd, scalar, NativeAOT and Mono AOT; CI checks every one of these paths on each push. The full methodology is in the README.

Packages

Sdcb.SimdCodecs (facade), .Abstractions, .Jpeg, .Jpeg.GainMap, .Jpeg.IccInjector, .Png, .Tiff, .Webp, .Gif, .Jpeg2000, .Jbig2, .Exif, .Color.Icc. All 13 are on NuGet and MIT-licensed.

Known limits, such as APNG frames, BigTIFF and 12-bit JPEG, are listed in docs/known-gaps.md (in Chinese).