Repository navigation
Sdcb.SimdCodecs 2.0.0 reshapes the shared API around one facade, ImageCodecs, and makes every codec do the same things: decode to any pixel layout, scaled or cropped, upright by the EXIF orientation, with its metadata; and encode from any pixel layout through one encoder API. Most 1.x code needs a rename and nothing else; every 8-bit decode output is byte-for-byte what 1.x produced (HEIF Main 10 aside, see below).
dotnet add package Sdcb.SimdCodecsusing 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);Upgrading: docs/migrating-to-2.0.md. Coming from ImageSharp: the migration table in the README.
Breaking changes
- The facade class
SimdCodecsis nowImageCodecs. A class named like its namespace segment failed to bind (CS0234) in any project whose namespace containsSimdCodecs. No alias is kept. ImagePixelFormat(enum) is replaced byPixelFormat(a readonly record struct):PixelFormat.Rgba32,.Bgra32,.Rgb24,.Bgr24,.Gray8are the same layouts.PixelConverter.BytesPerPixel(format)becomesformat.BytesPerPixel.ImageCodecs.TryDecodeIntoRgba8is removed from the facade: useImageCodecs.TryDecodeInto(data, buffer, 0, PixelFormat.Rgba32), orImageCodecs.Decode(data, PixelFormat.Rgba32)without a buffer of your own. The per-codecXxxImageDecoder.TryDecodeIntoRgba8methods stay.IDecodedImageisIDisposable; readingPixelsafterDisposethrowsObjectDisposedException.RasterImage: the constructor becomesRasterImage.Wrap(…);image.IccProfilebecomesimage.Metadata.Icc(ICC, Exif and XMP travel together inImageMetadata);ExpandToRgba8becomesTryCopyTo(destination, 0, PixelFormat.Rgba32).IImageDecoderhas new static abstract members for third-party codecs:Decode,TryGetClosestSize,TryReadMetadata,TryDecodeThumbnail, andDecodeOptions?/CancellationTokenparameters onTryDecode/TryDecodeInto.ImageDecoderFallback(namespaceSdcb.SimdCodecs.Abstractions.CodecSupport) has one-line implementations.ImageDecoderRegistrationis built with an object initializer; use each codec'sXxxImageDecoder.Registration.- Encoders check their options type: passing another format's options (e.g.
JpegEncodeOptionsto WebP) throwsArgumentExceptioninstead of silently using defaults.nullstill means the defaults. - WebP with no encode options is lossy (
WebpLossyEncodeOptionsdefaults); passnew WebpLosslessEncodeOptions()for lossless. (Affects the 2.0 previews, which wrote lossless.) - HEIF Main 10 decodes to 16-bit samples (
SampleFormat.UInt16,BitDepth10) instead of 8-bit.TryDecodeIntoto an 8-bit format keeps the frozen 1.x conversion, so it can differ by one code value from narrowing the 16-bit image. ImageInforeports more (ColorEncoding,Resolution,Orientation,HasThumbnail): code comparing wholeImageInfovalues sees, for example, a TIFF's 96 dpi.
New
- One-call decode with the reason on failure:
ImageCodecs.Decode(data)returns anIDecodedImage,ImageCodecs.Decode(data, PixelFormat)aPixelBufferin any layout (BGR and premultiplied included). They throwInvalidDataExceptionnaming the format and the codec's reason, andNotSupportedExceptionfor data no codec takes (naming whatDetectFormatsaw; for HEIF, that the package must be installed and registered). - Every
PixelFormatinto your buffer: 16-bit (Rgba64,Gray16, …) and premultiplied (Bgra32Premultipliedfor WPF / Direct2D / Skia, …) layouts from every codec, at any stride. DecodeOptions:ScaleDenominator: any power of two for every codec. JPEG 1/2–1/8 (DCT domain) and JPEG 2000 resolution levels are native; PNG (non-interlaced), lossy WebP and HEIF scale while decoding without holding the full frame; the rest decode in full and apply a frozen box filter (gamma space, alpha-weighted). It never throws for a valid power of two.TryGetClosestSizereports the native sizes.Region: a crop for every codec. TIFF and HEIF read only the strips or tiles it touches; JPEG, PNG and WebP convert only its rows. PNG and WebP stop at the region's last row and do not read or validate the data below it, so a region can decode from a file whose full decode fails.ApplyOrientation: EXIF / TIFF orientation applied (the region is in display coordinates; the Exif Orientation in the output metadata is reset to 1).GetOutputSize(info)sizes a buffer;PixelTransform.Orientturns pixels decoded without it.Pool: pixels rented from anArrayPool<byte>, returned onDispose.
- Metadata:
TryReadMetadata(Exif, XMP, ICC as raw blocks),TryDecodeThumbnail(Exif IFD1, HEIFthmb),IDecodedImage.Metadata. A JPEG's Extended XMP is joined into its XMP packet.ColorEncodingreports Display P3 / BT.2020 primaries from an ICC profile. ImageDecoder/ImageFrame: a WIC-style session over aStream, a memory-mapped file or memory (Open,OpenAsync), with per-frameDecodeandCopyPixels(region, format, buffer, stride).IImageEncoder,ImageCodecs.Encode/CreateEncoder: every encoder through one API, from everyPixelFormat, rows at any stride, metadata written; an encoder session takes bands and animation frames.GetClosestInputFormatshows where an 8-bit format narrows.- PNG lossy palette quantization (
PngWriteOptions.Quantize, the pngquant use case): OKLab palette with edge-aware dithering, typically a quarter to a third of the truecolour file, smaller than pngquant's at equal SSIM on the benchmark corpus. IPlanarImageDecoder: HEIF's YCbCr planes as coded, for video pipelines and GPU upload.- Cancellation on every decode and encode entry point.
Fixes
- WebP lossless on arm64: the VP8L select predictor summed its four channel distances in a byte on AdvSimd, so distances over 255 wrapped and some lossless WebP files decoded to wrong pixels on arm64 (Apple Silicon, Android, ARM servers). Present since 1.0; x64 was unaffected. CI now runs the WebP tests on every SIMD tier, including arm64.
- JBIG2 and JPEG 2000 files from #1 that failed to decode now match jbig2dec and OpenJPEG.
Performance
Unchanged where not listed: same-run A/B against 1.x shows the full-image paths within noise. PNG decode on 8-bit RGB / RGBA photos is 0.88× libpng 1.6.39's time (record); the README has the table for every tuned codec.
Verification
- 7 728 tests with every oracle required (
REQUIRE_ORACLES=1); every vector kernel checked against its scalar twin on Vector256, Vector128, scalar and arm64 AdvSimd in CI (now including WebP and HEIF on every tier). - NativeAOT and Mono AOT (browser-wasm) output identical to the JIT across every facade entry point.
Known gaps
Listed in docs/known-gaps.md. For 2.x: true streaming input (a Stream is still read into memory), row-streaming encoders, JPEG region decode skipping entropy decoding by restart interval, BoxDownscaler speed.