Skip to content

v2.0.0: one facade, every layout, scaled / region / oriented decode

Latest

Choose a tag to compare

@sdcb sdcb released this 09 Nov 15:17
· 3 commits to main since this release

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.SimdCodecs
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);

Upgrading: docs/migrating-to-2.0.md. Coming from ImageSharp: the migration table in the README.

Breaking changes

  • The facade class SimdCodecs is now ImageCodecs. A class named like its namespace segment failed to bind (CS0234) in any project whose namespace contains SimdCodecs. No alias is kept.
  • ImagePixelFormat (enum) is replaced by PixelFormat (a readonly record struct): PixelFormat.Rgba32, .Bgra32, .Rgb24, .Bgr24, .Gray8 are the same layouts. PixelConverter.BytesPerPixel(format) becomes format.BytesPerPixel.
  • ImageCodecs.TryDecodeIntoRgba8 is removed from the facade: use ImageCodecs.TryDecodeInto(data, buffer, 0, PixelFormat.Rgba32), or ImageCodecs.Decode(data, PixelFormat.Rgba32) without a buffer of your own. The per-codec XxxImageDecoder.TryDecodeIntoRgba8 methods stay.
  • IDecodedImage is IDisposable; reading Pixels after Dispose throws ObjectDisposedException.
  • RasterImage: the constructor becomes RasterImage.Wrap(…); image.IccProfile becomes image.Metadata.Icc (ICC, Exif and XMP travel together in ImageMetadata); ExpandToRgba8 becomes TryCopyTo(destination, 0, PixelFormat.Rgba32).
  • IImageDecoder has new static abstract members for third-party codecs: Decode, TryGetClosestSize, TryReadMetadata, TryDecodeThumbnail, and DecodeOptions? / CancellationToken parameters on TryDecode / TryDecodeInto. ImageDecoderFallback (namespace Sdcb.SimdCodecs.Abstractions.CodecSupport) has one-line implementations. ImageDecoderRegistration is built with an object initializer; use each codec's XxxImageDecoder.Registration.
  • Encoders check their options type: passing another format's options (e.g. JpegEncodeOptions to WebP) throws ArgumentException instead of silently using defaults. null still means the defaults.
  • WebP with no encode options is lossy (WebpLossyEncodeOptions defaults); pass new WebpLosslessEncodeOptions() for lossless. (Affects the 2.0 previews, which wrote lossless.)
  • HEIF Main 10 decodes to 16-bit samples (SampleFormat.UInt16, BitDepth 10) instead of 8-bit. TryDecodeInto to an 8-bit format keeps the frozen 1.x conversion, so it can differ by one code value from narrowing the 16-bit image.
  • ImageInfo reports more (ColorEncoding, Resolution, Orientation, HasThumbnail): code comparing whole ImageInfo values sees, for example, a TIFF's 96 dpi.

New

  • One-call decode with the reason on failure: ImageCodecs.Decode(data) returns an IDecodedImage, ImageCodecs.Decode(data, PixelFormat) a PixelBuffer in any layout (BGR and premultiplied included). They throw InvalidDataException naming the format and the codec's reason, and NotSupportedException for data no codec takes (naming what DetectFormat saw; for HEIF, that the package must be installed and registered).
  • Every PixelFormat into your buffer: 16-bit (Rgba64, Gray16, …) and premultiplied (Bgra32Premultiplied for 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. TryGetClosestSize reports 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.Orient turns pixels decoded without it.
    • Pool: pixels rented from an ArrayPool<byte>, returned on Dispose.
  • Metadata: TryReadMetadata (Exif, XMP, ICC as raw blocks), TryDecodeThumbnail (Exif IFD1, HEIF thmb), IDecodedImage.Metadata. A JPEG's Extended XMP is joined into its XMP packet. ColorEncoding reports Display P3 / BT.2020 primaries from an ICC profile.
  • ImageDecoder / ImageFrame: a WIC-style session over a Stream, a memory-mapped file or memory (Open, OpenAsync), with per-frame Decode and CopyPixels(region, format, buffer, stride).
  • IImageEncoder, ImageCodecs.Encode / CreateEncoder: every encoder through one API, from every PixelFormat, rows at any stride, metadata written; an encoder session takes bands and animation frames. GetClosestInputFormat shows 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.