PeachImage 0.3.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package PeachImage --version 0.3.1
                    
NuGet\Install-Package PeachImage -Version 0.3.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PeachImage" Version="0.3.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PeachImage" Version="0.3.1" />
                    
Directory.Packages.props
<PackageReference Include="PeachImage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PeachImage --version 0.3.1
                    
#r "nuget: PeachImage, 0.3.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PeachImage@0.3.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PeachImage&version=0.3.1
                    
Install as a Cake Addin
#tool nuget:?package=PeachImage&version=0.3.1
                    
Install as a Cake Tool

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 is not yet implemented (decode-only for animation).
  • 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, and both 8-bit and 10-bit depth. Animated AVIF, film grain synthesis, gain maps, 12-bit depth, and palette/IntraBC mode remain unimplemented and throw a clear AvifUnsupportedFeatureException rather than a silently wrong result. Encode is implemented for lossy, 8-bit, 4:2:0, opaque still images only (a single av01 item; no HEIF grid/avis animation on the output side even though decode supports reading them, and no partition-tree size search yet — every block is a fixed 8x8 with a real intra-mode decision among DC/vertical/horizontal/smooth/Paeth candidates). Alpha-bearing sources and higher bit depths are rejected with a clear exception rather than silently dropped or downsampled.
  • 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.
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.
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();
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 });

Animated images

Multi-frame formats (GIF, and WebP for decode — animated WebP encode isn't implemented yet) 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 });

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;

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

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

// Or pick a specific filter.
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.
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");

Building & testing

dotnet build PeachImage.slnx
dotnet test PeachImage.slnx

The first dotnet test run automatically fetches JPEG, BMP, and PNG 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) 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.

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.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on PeachImage:

Package Downloads
PeachPDF

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.6 3,406 9/16/2026
0.4.5 209 9/15/2026
0.4.4 127 9/15/2026
0.4.3 117 9/15/2026
0.4.2 3,985 8/26/2026
0.4.1 837 8/23/2026
0.3.1 122 8/21/2026
0.3.0 109 8/20/2026
0.2.2 284 8/17/2026
0.2.1 119 8/17/2026
0.2.0 129 8/17/2026
0.1.2 130 8/15/2026
0.1.0 125 8/15/2026