Skip to content

About

Pure .NET image format readers and writers for commonly used image formats on the web

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

PeachImage

Pure .NET image format readers and writers for commonly used image formats on the web.

Targets .NET 8.0 and .NET 10.0. No native interop — every codec is managed code, using modern .NET APIs (System.Runtime.Intrinsics, Span<T>/ReadOnlySpan<T>) for performance instead of P/Invoke.

Status

  • JPEG: decode (baseline sequential + progressive, grayscale/YCbCr/RGB/CMYK/YCCK, all standard chroma subsampling, restart markers) and encode (baseline sequential, grayscale/YCbCr) are implemented.
  • BMP: decode (OS/2 1.x/2.x and Windows BITMAPINFOHEADER through BITMAPV5HEADER variants, 1/4/8bpp indexed color, 16/24/32bpp direct color, RLE4/RLE8 compression, arbitrary BI_BITFIELDS/BI_ALPHABITFIELDS masks) and encode (24bpp truecolor, 8bpp indexed grayscale with optional RLE8, 32bpp with an explicit alpha channel via BITMAPV4HEADER + BI_BITFIELDS) are implemented, including explicit alpha-channel support on both sides.
  • PNG: decode and encode for all 5 color types (grayscale, truecolor, palette, grayscale+alpha, truecolor+alpha) at every valid bit depth (1/2/4/8/16 — including via Gray16/Rgb48/Rgba64 pixel formats), Adam7 interlacing, palette + tRNS transparency (both per-entry and single-color-key), optional opt-in gamma correction (PngDecoderOptions.ScreenGamma), and the common ancillary chunks (gAMA/cHRM/sRGB/iCCP/pHYs/tEXt/zTXt/iTXt/tIME/bKGD). Encoding can build an indexed palette automatically (PngEncoderOptions.ColorMode, default Auto): lossless whenever the source has at most MaxColors (default 256) distinct opaque colors and binary alpha, otherwise falling back to grayscale/truecolor(+alpha) unless ColorMode = Indexed forces palette output via median-cut quantization with optional Floyd-Steinberg dithering (Dither) — the same quantizer GIF encoding uses.
  • GIF: decode (GIF87a/GIF89a, interlacing, transparency, multi-frame animation with per-frame disposal methods and the NETSCAPE2.0 loop count via AnimatedImage.Load) and encode (median-cut palette quantization, optional Floyd-Steinberg dithering, animation) are implemented.
  • WebP: decode is implemented for both of WebP's bitstream codecs — VP8 (lossy) and VP8L (lossless) — including alpha (ALPH chunk / VP8L's own alpha) and animation (via AnimatedImage.Load, including the loop count) in the RIFF "simple" and "extended" container formats. Encode supports both bitstreams: lossless (VP8L, the default) with predictor-transform selection, palette/color-indexing detection, subtract-green, and a color cache; and lossy (VP8, opt in via WebpEncoderOptions { Lossless = false }) with quality-driven quantization. Alpha-bearing sources always encode as VP8L regardless of Lossless, since lossy WebP's alpha channel isn't implemented yet. Animated WebP encode (VP8X + ANIM/ ANMF, via AnimatedImage.Save) is also implemented, reusing the same per-frame VP8/VP8L encoder as still images; every frame is written full-canvas/non-blending, since AnimatedImageFrame only ever carries a fully composited frame.
  • AVIF: decode is implemented for baseline still images — intra-frame AV1, the full in-loop filter chain (deblocking, CDEF, loop restoration), HEIF grid composite images, alpha via the auxiliary-item mechanism, both 8-bit and 10-bit depth, and screen-content tools (palette mode, IntraBC). Animated AVIF, film grain synthesis, gain maps, and 12-bit depth remain unimplemented and throw a clear AvifUnsupportedFeatureException rather than a silently wrong result. Encode is implemented for 8-bit still images (a single av01 item, or two — color plus an iref auxl-referenced monochrome alpha item — for a source with real transparency; no HEIF grid/avis animation on the output side even though decode supports reading them). Both a lossy path (Quality, DCT_DCT, fixed 4:2:0 chroma, every luma block a fixed 8x8 leaf) and a lossless path (Lossless, AV1's Walsh-Hadamard coded-lossless mode, forcing 4x4 transforms, full-resolution 4:4:4 chroma via an identity RGB→YUV matrix, a real cost-based partition-tree size search up to 64x64, and screen-content tools) are supported; lossless round-trips every pixel format exactly — see AvifEncoderOptions.Lossless's remarks. Every leaf gets a real, cost-based intra-mode search (all 13 AV1 intra modes plus angle_delta for the 8 directional ones, plus FILTER_INTRA) for luma; lossless chroma gets the same real mode search (non-lossless chroma stays DC_PRED, since its transform type is mode-dependent and this encoder's non-lossless chroma path doesn't yet implement the non-DCT transforms that searching a real mode there would need). Higher bit depths are rejected with a clear exception rather than silently dropped or downsampled. The forward RGB→YUV conversion, forward transform, and quantizer all use SIMD-tiered kernels (scalar/Vector128/Vector256, matching the JPEG/WebP encoders' own dispatch pattern) and pool their per-block working buffers rather than allocating per block.
  • TIFF: decode only (no encode). Covers both byte orders (II/MM), uncompressed/LZW/PackBits and CCITT (Modified Huffman, Group 3 one- and two-dimensional, Group 4; either FillOrder) compression, 1/2/4/8/16-bit depth, and grayscale (WhiteIsZero/BlackIsZero), RGB (with optional straight or premultiplied alpha), palette, and CMYK color, including the Predictor=2 horizontal-differencing variant LZW-compressed files commonly use. Tiled organization, planar (non-chunky) storage, any compression other than none/CCITT/LZW/PackBits, BigTIFF, floating-point/signed samples, and photometric interpretations outside that set (YCbCr, LogLuv, Lab) are deliberately out of scope and throw a clear TiffUnsupportedFeatureException rather than a silently wrong result — the goal is correctness on real-world scanner/export-tool output, not every TIFF extension ever specified. An embedded ICC profile (tag 34675) is extracted into Image.Metadata, same as JPEG/PNG.
  • JPEG XL: decode and still-image encode (animation is decode-only; see the encoder paragraph below). A from-scratch managed decoder for the ISO/IEC 18181 codestream and ISOBMFF container: lossless and lossy Modular (every predictor including the weighted predictor, RCT, palette/delta-palette and squeeze transforms, progressive passes) and VarDCT (every DCT size from 8x8 to 256x256, the rectangular, IDENTITY, DCT2x2/4x4/4x8 and AFV transforms, chroma-from-luma, adaptive quantization, custom dequantization tables, Gaborish and the edge-preserving filter), patches, splines, synthesized noise, 2x/4x/8x upsampling, DC frames, extra channels (alpha, spot colours rendered like the reference decoder, black/CMYK), premultiplied alpha (returned straight), and JPEG recompressions (YCbCr with any chroma subsampling, grayscale or RGB). Files made by losslessly recompressing a JPEG can also be turned back into that JPEG, byte for byte, with JxlJpegReconstruction, and baseline JPEGs can be recompressed into such files with JxlJpegRecompression (see below). Multi-frame images use full frame blending (cropped frames, every blend mode, reference frames) and decode through AnimatedImage as composited RGBA frames. XYB images are rendered in the file's own colour space: sRGB, any enumerated or custom primaries and white point, linear, gamma, BT.709, DCI, PQ and HLG, or an embedded RGB/gray ICC profile (CMYK images are returned as Cmyk32 with the profile attached; use Image.ConvertToSrgb). Samples above 8 bits decode to the 16-bit formats, floating-point or deeper samples to the new GrayF32/RgbF32/RgbaF32 formats. EXIF and XMP boxes (including Brotli-compressed ones) and the embedded ICC profile are exposed through Image.Metadata, and orientation is applied. Not supported (throws a clear JxlUnsupportedFeatureException): 32-bit integer samples, which the reference decoder rejects too. Grayscale XYB images with a non-D65 white point are rendered as plain luminance. Group decoding, the restoration filters, colour conversion and the splines/noise/upsampling stages run on all cores and use Vector<T> kernels (AVX2 on x64, NEON on ARM64). The encoder (JxlEncoderOptions) writes still images from Gray8, Rgb24, Rgba32, their 16-bit counterparts and the float formats GrayF32/RgbF32/RgbaF32 (floats are sRGB-encoded, may exceed 1, and are kept bit for bit when lossless) (a fully opaque RGBA image is stored without alpha; alpha is straight). Lossless (Lossless = true) uses the Modular coder with a reversible YCoCg colour transform, per-channel gradient prediction and rANS entropy coding, in 256x256 groups coded in parallel; an ICC profile on the image is embedded and restored. Lossy (the default, Quality 0-100) uses VarDCT: XYB colour, a separately quantized DC image, and context-modelled rANS coefficients, with a per-block choice (Effort, 1-9) among 8x8, 16x16, 32x32 and the 16x8/8x16/32x16/16x32 transforms, per-tile chroma-from-luma, the decoder's edge-preserving filter, Gaborish smoothing compensated by an encoder-side inverse filter at high qualities, and an optional adaptive quantization field (AdaptiveQuantization, off by default because it made files larger at the same SSIMULACRA2 score); images carrying an ICC profile are colour-managed to sRGB first, since XYB assumes sRGB primaries. EXIF and XMP are written to container boxes and the EXIF orientation is recorded in the codestream header. Output is checked against libjxl (see below). At equal SSIMULACRA2 score the lossy files are within a few percent of libjxl's effort 7 (see LIBRARY_COMPARISON.md).
  • JPEG 2000: decode only (ITU-T T.800 Part 1). JP2 files and raw codestreams (.j2k/.j2c), reversible 5/3 and irreversible 9/7 wavelets, tiles and tile-parts, precincts, quality layers, all five progression orders and progression order changes, every code-block style (bypass, reset, termination on each pass, vertically causal contexts, predictable termination, segmentation symbols), region-of-interest maxshift, SOP/EPH markers, packed packet headers (PPM/PPT), image and tile offsets, sub-sampled components, 1 to 16-bit samples (signed or unsigned), the reversible and irreversible component transforms, and the JP2 colr (enumerated sRGB/gray/sYCC/CMYK or an ICC profile), pclr/cmap palette and cdef channel-definition boxes (opacity channels become alpha). Jpeg2000DecoderOptions.ReduceResolutionLevels decodes at half, quarter, ... size by skipping the finest wavelet levels. Part 2 (JPX) extensions and sample depths above 16 bits throw a clear Jpeg2000UnsupportedFeatureException. Output was checked sample for sample against OpenJPEG.
  • JBIG2: decode only (ITU-T T.88). Standalone .jb2/.jbig2 files (sequential and random-access organisation) and the embedded segment streams of PDF files. Generic regions (all four templates, typical prediction, moved adaptive pixels, MMR), generic refinement regions, symbol dictionaries (arithmetic and Huffman, refinement/aggregate coding, retained contexts, custom tables), text regions (arithmetic and Huffman, every reference corner, transposed, refined instances), pattern dictionaries and halftone regions, striped pages with unknown height, and intermediate regions. Only the first page is decoded; the extended 12-pixel generic template is not supported. Output was checked against two independent decoders (jbig2dec through MuPDF, and PDFium).
  • CCITT fax: decode only. Inside TIFF (see above) and as bare Group 3 / Group 4 streams such as the data of a PDF CCITTFaxDecode filter, selected with CcittFaxDecoderOptions (see below). The uncompressed extension mode is not supported.
  • Other formats are not yet implemented. The public API (Image, AnimatedImage for multi-frame formats like GIF) is designed to support them without breaking changes when they're added. Codec selection is internal — there's no format-specific type or registration step in the public API.

See LIBRARY_COMPARISON.md for performance numbers against SkiaSharp.

Installing PeachImage

Install the PeachImage package from nuget.org

dotnet add package PeachImage

Usage

Single-frame images

The format is auto-detected from the file's contents for every operation below — no setup call needed.

using PeachImage;
using PeachImage.Formats.Jpeg;

// Load, inspect, and convert between formats.
using var image = Image.Load("photo.webp");
Console.WriteLine($"{image.Width}x{image.Height} {image.PixelFormat}");

using var output = File.Create("resaved.jpg");
image.Save(output, "jpeg", new JpegEncoderOptions { Quality = 85 });
using PeachImage;

// Read dimensions/format without decoding pixel data.
using var stream = File.OpenRead("photo.avif");
ImageInfo info = Image.Identify(stream);
Console.WriteLine($"{info.Width}x{info.Height} {info.PixelFormat} ({info.FormatName})");
using PeachImage;

// Zero-copy access to the decoded pixel buffer.
using var image = Image.Load("photo.png");
Span<byte> pixels = image.GetPixelSpan();
Span<byte> firstRow = image.GetRowSpan(0);

Bytes already in memory (e.g. a buffered upload) load directly — no need to wrap them in a MemoryStream first; a byte[] converts implicitly to ReadOnlySpan<byte>, and decoding reads straight out of that memory with no intermediate copy:

using PeachImage;

byte[] uploadedBytes = await ReadUploadIntoMemoryAsync();
using var image = Image.Load(uploadedBytes);

SaveAsync exists for async I/O call paths. Encoding itself is CPU-bound, not I/O-bound, so only the actual stream/file write is awaited — same as LoadAsync otherwise:

using PeachImage;

using var output = File.Create("resaved.jpg");
await image.SaveAsync(output, "jpeg", new JpegEncoderOptions { Quality = 85 });

Disposal & buffer pooling

Image implements IDisposable: most instances rent their pixel buffer from a shared ArrayPool, and Dispose returns it for reuse by the next decode/resize/etc. This is a performance optimization, not a correctness requirement — an un-disposed Image is simply garbage-collected like any other object, with no leak or corruption risk. It matters most under concurrent load (e.g. a service resizing many uploads at once), where reusing pooled buffers meaningfully cuts allocation and GC pressure compared to a fresh buffer per call. AnimatedImage/AnimatedImageFrame don't need disposal: a frame pulled from AnimatedImage.Frames aliases decoder-internal state rather than owning a pooled buffer (disposing it anyway is a safe no-op), and only AnimatedImageFrame.Clone()/Image.Clone() results own one.

Animated images

Multi-frame formats (GIF and WebP) use AnimatedImage instead, with the same load/save shape:

using PeachImage;
using PeachImage.Formats.Gif;

var animation = AnimatedImage.Load("clip.gif");

foreach (AnimatedImageFrame frame in animation.Frames)
{
    Console.WriteLine($"{frame.Duration.TotalMilliseconds}ms, disposal={frame.Disposal}");
}

using var output = File.Create("resaved.gif");
animation.Save(output, "gif", new GifEncoderOptions { MaxColors = 128, Dither = true });

Animated WebP works the same way (AnimatedImage.Save(stream, "webp", new WebpEncoderOptions())); WebP's single dispose-to-background bit means FrameDisposalMethod.RestoreToPrevious collapses to DoNotDispose on round-trip through WebP, unlike GIF which represents all three disposal methods natively.

Resizing

Image.Resize/AnimatedImage.Resize support 15 resampling filters via ResamplingFilter — Bicubic is the default; also available: Box, CatmullRom, Hermite, Lanczos2/Lanczos3/Lanczos5/Lanczos8, MitchellNetravali, NearestNeighbor, Robidoux, RobidouxSharp, Spline, Bilinear, and Welch.

using PeachImage;

using var image = Image.Load("photo.jpg");

// Bicubic by default.
using var thumbnail = image.Resize(200, 150);

// Or pick a specific filter.
using var sharpened = image.Resize(200, 150, new ResizeOptions { Filter = ResamplingFilter.Lanczos3 });

// ResizeMode.Max treats width/height as a bounding box instead of an exact target: scales down to the
// largest size that fits while preserving aspect ratio, and never upscales — if the source already fits,
// the same instance is returned unchanged rather than allocating a needless copy (so this may end up
// disposing `image` itself — safe, since disposing twice is a no-op, but don't keep using `image`
// afterward without checking for that case first).
using var thumbnailWithinBox = image.Resize(200, 200, new ResizeOptions { Mode = ResizeMode.Max });

AnimatedImage.Resize resizes every frame — lazily, as Frames is enumerated — preserving each frame's duration and disposal method; ResizeOptions.Mode works the same way there too:

using PeachImage;

var animation = AnimatedImage.Load("clip.gif");
var resized = animation.Resize(160, 120, new ResizeOptions { Filter = ResamplingFilter.MitchellNetravali });

using var output = File.Create("resized.gif");
resized.Save(output, "gif");

Decoding images from PDF files

The three formats PDF uses for bilevel and JPEG 2000 images decode through the same Image.Load. Standalone JBIG2 and JPEG 2000 files are recognized by their signatures; the data of a PDF stream has no signature (a CCITTFaxDecode stream is bare fax data, a JBIG2Decode stream is a run of segments with no file header), so pass the matching options object, filled from the stream's DecodeParms, to select the decoder:

using PeachImage;
using PeachImage.Formats.Ccitt;
using PeachImage.Formats.Jbig2;
using PeachImage.Formats.Jpeg2000;

// /Filter /CCITTFaxDecode  /DecodeParms << /K -1 /Columns 2480 /Rows 3508 /BlackIs1 false >>
using var fax = Image.Load(new MemoryStream(ccittData), new CcittFaxDecoderOptions { K = -1, Columns = 2480, Rows = 3508 });

// /Filter /JBIG2Decode  /DecodeParms << /JBIG2Globals 12 0 R >>
using var jbig2 = Image.Load(new MemoryStream(pageData), new Jbig2DecoderOptions { Embedded = true, Globals = globalsData });

// /Filter /JPXDecode: the colour space comes from the PDF, so skip the JP2 boxes' interpretation.
using var jpx = Image.Load(new MemoryStream(jpxData), new Jpeg2000DecoderOptions { IgnoreColorSpace = true });

CCITT and JBIG2 images come out as Gray8 with black pixels as 0 (the PDF convention for a default /Decode); CcittFaxDecoderOptions.BlackIs1 and Jbig2DecoderOptions.InvertOutput flip that. Fax options mirror the PDF filter parameters (K, Columns, Rows, EndOfLine, EncodedByteAlign, EndOfBlock, BlackIs1, DamagedRowsBeforeError); Rows = 0 decodes until the data ends.

Recovering the original JPEG from a JPEG XL file

JPEG XL files made from a JPEG (as cjxl does for JPEG input) carry the information needed to rebuild that exact JPEG file, including its progressive scans, restart intervals, Huffman tables, ICC profile, Exif and XMP. JxlJpegReconstruction returns those original bytes without decoding any pixels, which is useful where a JPEG can be embedded as-is (for example in a PDF) instead of being decoded and encoded again:

using var stream = File.OpenRead("photo.jxl");
if (JxlJpegReconstruction.TryReconstructJpeg(stream, out byte[]? jpeg))
{
    File.WriteAllBytes("photo.jpg", jpeg);   // identical to the JPEG that was recompressed
}

HasJpegReconstructionData tells whether a file qualifies without rebuilding it, and ReconstructJpeg throws JxlUnsupportedFeatureException for files that were not made from a JPEG (those decode through Image.Load as usual).

Recompressing a JPEG into JPEG XL

JxlJpegRecompression goes the other way, as cjxl does for JPEG input: it re-codes a JPEG's quantized DCT coefficients with JPEG XL's entropy coder, without decoding any pixels and without any loss, so the JPEG file can later be recovered unchanged. The JPEG XL file is typically a fifth to a third smaller than the JPEG, and it also decodes like any JPEG XL image, to the pixels the JPEG decodes to:

using var input = File.OpenRead("photo.jpg");
if (JxlJpegRecompression.TryRecompressJpeg(input, out byte[]? jxl))
{
    File.WriteAllBytes("photo.jxl", jxl);
}

Baseline (sequential Huffman, 8-bit) JPEGs with one component or with Y, Cb and Cr components are supported, with 4:4:4, 4:2:2, 4:4:0 or 4:2:0 sampling, restart intervals, and any metadata (ICC profile, Exif, XMP, comments, other markers, even stray bytes between markers or after the image). Every result is reconstructed once before it is returned and compared with the input, so a JPEG that cannot come back byte for byte is declined (TryRecompressJpeg returns false, RecompressJpeg throws JxlUnsupportedFeatureException) rather than silently changed. Progressive, arithmetic-coded, 12-bit, RGB and CMYK JPEGs are not supported yet; decode and encode those with Image.Load and Image.Save instead.

Converting pixel formats

Image.ConvertTo converts between the gray, RGB and RGBA formats at 8-bit, 16-bit and 32-bit float depth (for example an HDR RgbaF32 JPEG XL decode to Rgb24 before encoding it). Integer targets round and clamp, colour to gray uses BT.601 luma, and a target without alpha discards the alpha channel. Values are converted as stored, with no tone mapping. CMYK images go through ConvertToSrgb instead.

using var hdr = Image.Load("photo.jxl");           // may be RgbaF32 for HDR files
using var jpegReady = hdr.ConvertTo(PixelFormat.Rgb24);
jpegReady.Save("photo.jpg");

Building & testing

dotnet build PeachImage.slnx
dotnet test PeachImage.slnx

The first dotnet test run automatically fetches JPEG, BMP, PNG, TIFF, and JPEG XL test corpora (the Imazen codec-corpus conformance sets, image-rs/jpeg-decoder's test assets, and — for BMP — the bmp-conformance subset of codec-corpus, itself generated from Jason Summers' bmpsuite; for PNG — the pngsuite subset of codec-corpus, a mirror of Willem van Schaik's classic PngSuite conformance set; for TIFF — the tiff-conformance subset of codec-corpus, sourced from libtiff's, image-tiff's, and image-rs's own test suites; for JPEG XL — the libjxl conformance test cases and testdata, each decoded and compared with its reference image under the suite's own thresholds) into the gitignored tests/corpus/ directory — no separate script needed. Set PEACHIMAGE_SKIP_CORPUS_FETCH=1 to skip network access; corpus-driven tests report as skipped rather than failing.

TIFF also has a decode-correctness check against ffmpeg's independent TIFF decoder (SkiaSharp has no TIFF codec, so it can't serve as the differential oracle the other formats' corpus tests use). This baseline is checked in (tests/PeachImage.Tests/Formats/Tiff/Corpus/TiffFfmpegReference.baseline.tsv) and regenerating it requires ffmpeg/ffprobe on PATH — set PEACHIMAGE_TIFF_FFMPEG_BASELINE=write and run dotnet test --filter TiffFfmpegReferenceTests, then review the diff before committing. Normal test runs never invoke ffmpeg; they only compare against the checked-in baseline.

AVIF and WebP decode are additionally checked against ffmpeg's own, independent decoders (libdav1d for AVIF; libwebp itself for WebP) via a checked-in pixel baseline (AvifFfmpegReference.baseline.tsv/ WebpFfmpegReference.baseline.tsv) — a real correctness oracle, not just a self-referential "did the decoder's output change" regression check. For AVIF this is the only independent oracle available at all (SkiaSharp, this repo's oracle for the other bitmap formats, has no AVIF codec); for WebP it's a supplementary cross-check alongside the existing SkiaSharp differential, scoped to single-frame VP8L (lossless) files, where the comparison can be exact rather than tolerance-based (see AvifFfmpegReferenceBaseline's and WebpFfmpegReferenceBaseline's own remarks for why AVIF needs a tolerance and WebP's lossy bitstream is out of scope for this specific check). Normal test runs only read the checked-in baseline and never invoke ffmpeg; regenerating it after a corpus or decoder change requires ffmpeg/ffprobe on PATH and PEACHIMAGE_AVIF_FFMPEG_BASELINE=write/PEACHIMAGE_WEBP_FFMPEG_BASELINE=write respectively.

JPEG XL decode is additionally checked against ffmpeg built with libjxl (the reference implementation) as a live oracle, and so is JPEG XL encode (libjxl must decode every file the encoder writes: lossless pixel for pixel, lossy to within rounding of this decoder). For decode, the tests encode synthetic images with libjxl (lossless and lossy, every effort level, 8/16-bit and float input, alpha, HDR transfer functions and wide-gamut primaries) and require the decoder to match libjxl's own decode. They skip when ffmpeg is not on PATH.

With libjxl's reference decoder djxl available (put it on PATH or point PEACHIMAGE_DJXL at it), every file of the JPEG XL corpus, and every JPEG XL file in the test project, is also decoded by both and compared sample for sample at 16-bit (animations as float): almost everything agrees to within one 16-bit unit, and the few known, understood differences have their own tolerances.

Benchmarking

dotnet run -c Release --project bench/PeachImage.Benchmarks

Compares PeachImage's decode/encode throughput against SkiaSharp (a dev-only dependency of the benchmark project only — never referenced by the shipped library). See LIBRARY_COMPARISON.md for the latest results.

License

MIT — see LICENSE. One algorithm's numerical structure (the AAN fast DCT/IDCT butterfly wiring) was referenced from libjpeg-turbo during implementation; see THIRD-PARTY-LICENSES.md.

About

Pure .NET image format readers and writers for commonly used image formats on the web

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages