EasyImageSharp 1.1.0
dotnet add package EasyImageSharp --version 1.1.0
NuGet\Install-Package EasyImageSharp -Version 1.1.0
<PackageReference Include="EasyImageSharp" Version="1.1.0" />
<PackageVersion Include="EasyImageSharp" Version="1.1.0" />
<PackageReference Include="EasyImageSharp" />
paket add EasyImageSharp --version 1.1.0
#r "nuget: EasyImageSharp, 1.1.0"
#:package EasyImageSharp@1.1.0
#addin nuget:?package=EasyImageSharp&version=1.1.0
#tool nuget:?package=EasyImageSharp&version=1.1.0
<div align="center">
<img src="https://raw.githubusercontent.com/FarhanLodi/EasyImageSharp/main/src/EasyImageSharp/Assets/icon.png" alt="EasyImageSharp" width="128" height="128" />
EasyImageSharp
A complete 2D imaging library for .NET, written entirely in managed C#.
Ten codecs, a fluent processing pipeline, EXIF metadata, a document-imaging toolkit and ONNX tensor bridges — in one assembly, with no native dependencies and no licence key.
</div>
dotnet add package EasyImageSharp
using EasyImageSharp;
using EasyImageSharp.PixelFormats;
using EasyImageSharp.Processing;
using Image<Rgba32> image = Image.Load<Rgba32>("photo.jpg");
image.Mutate(ctx => ctx.AutoOrient().Resize(800, 0));
image.SaveAsWebp("thumbnail.webp");
Highlights
- Ten codecs, decode and encode. PNG and APNG, JPEG (baseline, progressive, CMYK), WebP (lossy, lossless, animated), GIF, BMP, TIFF (multi-page, CCITT G3/G4, JPEG-in-TIFF, and BigTIFF on read), TGA, Netpbm, QOI and ICO/CUR.
- One managed assembly. No native binaries and no per-architecture packages, so the same package publishes to Native AOT, trimmed, single-file, Alpine and ARM64 targets without a RID matrix.
- A fluent pipeline. Resize with 16 resamplers, crop, rotate, affine and projective transforms, colour matrices, convolution, blur and sharpen, edge detection, histogram equalisation and CLAHE, quantisation and dithering, 18 blend modes, and annotation drawing.
- Document imaging built in. Otsu, Sauvola, Niblack, Wolf-Jolion, Phansalkar, NICK and adaptive
thresholding; deskew; page detection and perspective correction; illumination correction; morphology;
connected components; text-line and word segmentation; and a one-call
PrepareForOcr()preset. - Metadata that survives. EXIF read and write with typed access, ICC and XMP passthrough, and
AutoOrient(). - Hardened against untrusted input. Size limits enforced before allocation, a closed exception contract, around 150 corrupt-input tests and a fuzz pass on every build.
- Fast, and checkable. SIMD kernels, pooled buffers, copy-on-write clones, row-parallel execution
and — on .NET 8 — a managed DEFLATE decoder for PNG. Every figure in Performance is
produced by a benchmark in
benchmarks/against a corpus a script rebuilds. - ONNX-ready. Image-to-tensor bridges in the core, plus an optional
EasyImageSharp.AIpackage with six pre-wired models and a checksum-verified model hub. - MIT, permanently. No revenue threshold, no commercial tier, no licence key.
Contents
Why · Install · Getting started · Recipes · Formats · Processing · Metadata · Untrusted input · Performance · AI · Packages · Deployment · Verification · Building · Community
Why EasyImageSharp
Everything in one managed assembly. Ten codecs, the processing pipeline, EXIF, document imaging and drawing live in a single DLL under 1 MB with no dependency beyond the framework. There are no native binaries, no per-architecture asset packages and no platform-specific build steps. A container image does not grow by tens of megabytes of native payload, and a deployment does not fail at run time because a shared object was missing for one architecture.
MIT, with no conditions attached. No revenue threshold, no build-time licence key, no separate commercial tier, no distinction between open- and closed-source use. The licence that applies to a hobby project is the licence that applies to a company, permanently. Compliance review is one line.
Document imaging is a first-class citizen. Most imaging libraries stop at resize, crop and filters,
so a document pipeline ends up gluing a general imaging library to a computer-vision toolkit. Here,
SauvolaThreshold, Deskew, DetectPage, CorrectPerspective, morphology, connected components,
illumination correction and text-line segmentation are ordinary operations on the same Mutate pipeline
as Resize — one dependency, one pixel type, no conversion layer.
page.Mutate(ctx => ctx.BackgroundNormalize(40).Deskew().SauvolaThreshold());
Designed for ONNX from the start. ToChwTensor and FromChwTensor handle layout and normalisation
for any model you bring, and the optional EasyImageSharp.AI package adds six
ready-made operations backed by a checksum-verified model hub. Classical and learned methods compose in
the same pipeline.
Install
dotnet add package EasyImageSharp # core library
dotnet add package EasyImageSharp.AI # optional ONNX-powered operations
Targets .NET 8.0 and .NET 10.0.
Getting started
using EasyImageSharp;
using EasyImageSharp.PixelFormats;
using EasyImageSharp.Processing;
// The format is detected from the bytes, never from the file extension.
using Image<Rgb24> image = Image.Load<Rgb24>("input.png");
Console.WriteLine($"{image.Width}x{image.Height} {image.Metadata.DecodedImageFormat?.Name}");
// Mutate edits in place; Clone returns a new image and leaves the source untouched.
image.Mutate(ctx => ctx.Resize(400, 0).Grayscale());
using Image<Rgb24> small = image.Clone(ctx => ctx.Resize(100, 0));
image.SaveAsJpeg("output.jpg");
await small.SaveAsync("small.png"); // format chosen from the extension
Image.Load(...)without a type argument decodes toRgba32.- Load from a path, stream or byte span; save to a path or stream.
LoadAsync,SaveAsyncand theSaveAs…Asyncfamily take aCancellationToken. - A zero width or height in
Resizepreserves the aspect ratio. - Images own their pixel buffers and implement
IDisposable— always useusing.
Recipes
Thumbnails with resource limits
using EasyImageSharp.Formats;
using EasyImageSharp.Formats.Webp;
var options = new DecoderOptions { MaxPixels = 50_000_000 };
using Image<Rgba32> image = Image.Load<Rgba32>(uploadedBytes, options);
using Image<Rgba32> thumb = image.Clone(ctx => ctx.Resize(new ResizeOptions
{
Size = new Size(320, 320),
Mode = ResizeMode.Crop,
Sampler = KnownResamplers.Lanczos3,
}));
thumb.SaveAsWebp("thumb.webp", new WebpEncoder { Quality = 82 });
Validating an untrusted upload
// Identify parses only the header and is never size-limited, so check the declared
// dimensions before committing to a decode.
ImageInfo info = await Image.IdentifyAsync(stream);
if ((long)info.Width * info.Height > 40_000_000)
{
throw new InvalidDataException($"{info.Width}x{info.Height} exceeds the supported size.");
}
stream.Position = 0;
try
{
using Image<Rgba32> image = await Image.LoadAsync<Rgba32>(stream);
image.Mutate(ctx => ctx.AutoOrient());
image.SaveAsJpeg("normalised.jpg");
}
catch (ImageFormatException ex) // unknown format, malformed data, or a size limit exceeded
{
Console.Error.WriteLine(ex.Message);
}
Re-encoding with options
using System.IO.Compression;
using EasyImageSharp.Formats.Jpeg;
using EasyImageSharp.Formats.Png;
using Image<Rgba32> image = Image.Load<Rgba32>("input.tif");
image.SaveAsJpeg("out.jpg", new JpegEncoder { Quality = 90, Progressive = true });
image.SaveAsPng("out.png", new PngEncoder { CompressionLevel = CompressionLevel.SmallestSize });
Preparing a scan for OCR
using Image<Rgb24> page = Image.Load<Rgb24>("scan.jpg");
page.Mutate(ctx => ctx
.BackgroundNormalize(40) // flatten uneven illumination
.Deskew() // projection-profile straightening
.MedianBlur(1) // remove speckle
.SauvolaThreshold()); // document-grade binarisation
page.SaveAsPng("clean.png");
// The same steps as a single preset:
page.Mutate(ctx => ctx.PrepareForOcr());
Rectifying a photographed document
using Image<Rgb24> photo = Image.Load<Rgb24>("desk-photo.jpg");
if (photo.DetectPage() is { } quad)
{
photo.Mutate(ctx => ctx.CorrectPerspective(quad));
}
Annotating detection results
image.Mutate(ctx =>
{
foreach (var (box, label) in detections)
{
ctx.DrawRectangle(Color.Lime, 2f, box);
ctx.DrawLabel(label, Color.Black, Color.Lime, box);
}
});
Pages and frames
using Image<Rgb24> document = Image.Load<Rgb24>("fax.tif");
for (int i = 0; i < document.Frames.Count; i++)
{
using Image<Rgb24> page = document.Frames.CloneFrame(i);
page.SaveAsPng($"page-{i:D3}.png");
}
Animated GIF, WebP and APNG frames are delivered fully composited, with disposal and blending applied.
Animated PNG
using EasyImageSharp.Formats.Png;
using EasyImageSharp.Metadata;
// APNG frames decode fully composited, one image frame per animation frame.
using Image<Rgba32> animation = Image.Load<Rgba32>("loop.png");
PngFrameMetadata first = animation.Frames[0].Metadata.GetPngMetadata();
Console.WriteLine($"{animation.Frames.Count} frames, first delay {first.FrameDelay} s");
animation.Metadata.GetPngMetadata().RepeatCount = 0; // 0 plays forever
animation.SaveAsPng("out.png", new PngEncoder { FrameDelay = 50 });
// A multi-frame image is written as an APNG. Export a frame for a still PNG.
using Image<Rgba32> still = animation.Frames.ExportFrame(0);
still.SaveAsPng("still.png");
Two limits are worth knowing before you encode one:
SaveAsPngon a multi-frame image writes an APNG rather than silently dropping every frame after the first. That is a behaviour change in 1.1.0;image.Frames.ExportFrame(0)is the opt-out, and a single-frame image is still written exactly as it was, with no animation chunks at all.- Animated output is truecolour or grayscale at 8 or 16 bits. A palette or sub-8-bit grayscale
ColorTypeon an animated image throwsNotSupportedException, because every frame would have to share one palette while the quantizer runs per frame.
Delay and disposal come from each frame's PngFrameMetadata, and PngEncoder.FrameDelay overrides
them for every frame. PngMetadata.AnimateRootFrame = false keeps the first frame out of the
animation, which is the still image an APNG-unaware reader shows.
Fast pixel access
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgb24> row = accessor.GetRowSpan(y);
for (int x = 0; x < row.Length; x++)
{
row[x] = new Rgb24(row[x].B, row[x].G, row[x].R);
}
}
});
The image[x, y] indexer bounds-checks every access; prefer ProcessPixelRows in hot paths.
Format support
| PNG | JPEG | WebP | GIF | BMP | TIFF | TGA | PNM | QOI | ICO | |
|---|---|---|---|---|---|---|---|---|---|---|
| Decode | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
| Encode | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
| Animation | ✔ | ✔ | ✔ | |||||||
| Multi-page | ✔ | ✔ |
| Format | Decode | Encode |
|---|---|---|
| PNG | All colour types, bit depths 1/2/4/8/16, Adam7 interlacing, palette and colour-key transparency, and APNG animation with per-frame offsets, delays, disposal and blending | All colour types and bit depths, palette output via quantisation, Adam7, selectable filtering, and APNG output (truecolour or grayscale at 8/16 bits) |
| JPEG | Baseline, extended sequential and progressive; all chroma subsampling with triangle upsampling for 4:2:2 and 4:2:0; restart markers; grayscale, YCbCr, RGB, Adobe CMYK and YCCK | Baseline and progressive, quality 1–100, 4:4:4 / 4:2:2 / 4:2:0 / 4:1:1 / 4:1:0, grayscale, RGB, CMYK, YCCK, optimised Huffman tables, restart intervals |
| WebP | Lossy (VP8), lossless (VP8L), alpha, animation with offsets, blending and disposal | Lossy and lossless, near-lossless, alpha, animation, quality and effort levels |
| GIF | GIF87a/89a, global and local palettes, interlacing, transparency, animation with disposal | LZW, global or per-frame palettes, transparency, delays, loop count |
| BMP | 1/4/8-bit palette, 16/24/32-bit, bitfields and alpha bitfields, RLE8/RLE4, OS/2 headers, both row orders | 1/4/8-bit palette, 16-bit, 24-bit, 32-bit with alpha |
| TIFF | Classic TIFF and BigTIFF, multi-page, both byte orders, strips and tiles, chunky and planar, None / LZW / Deflate / PackBits / CCITT G3 & G4 / JPEG, horizontal predictor, 1–32-bit samples (unsigned, signed, floating point), WhiteIsZero / BlackIsZero / palette / RGB(A) / CMYK / YCbCr / CIELab | Multi-page, None / LZW / Deflate / PackBits / CCITT G3 & G4, selectable bit depth, photometric and predictor |
| TGA | Types 1/2/3 and RLE variants, 8/15/16/24/32-bit, colour maps, either origin | 8/16/24/32-bit, raw or run-length |
| PNM | P1–P6 (ASCII and binary) and P7 PAM, 8- and 16-bit | PBM / PGM / PPM, plain or binary |
| QOI | Full specification | Byte-identical to the reference encoder |
| ICO / CUR | Multi-image icons with embedded BMP or PNG entries | PNG or 32-bit BMP entries, cursors with hotspots |
BigTIFF is read, not written. A version-43 file — 8-byte offsets, 64-bit directory entry counts,
20-byte entries and the LONG8 / SLONG8 / IFD8 field types — decodes like any other TIFF, with EXIF
and the rest of the metadata read at 64 bits. The encoder always writes classic TIFF. Because a file is
buffered whole before decoding, a BigTIFF must still be under 2 GiB; the point of the support is
compatibility with files written in the larger container, not multi-gigabyte imagery.
Not implemented, and reported as NotSupportedException with a message naming the feature:
arithmetic-coded, lossless and 12-bit JPEG; old-style JPEG-in-TIFF (compression 6); JBIG. HEIC/HEIF is
not planned (patent-encumbered), and AVIF would only ever ship as an opt-in add-on.
Processing
Every operation is available on the IImageProcessingContext passed to Mutate and Clone.
| Category | Operations |
|---|---|
| Geometry | Resize (Stretch / Max / Min / Pad / Crop / BoxPad / Manual, anchor positions, 16 resamplers, optional linear-light and premultiplied-alpha resampling), Crop, EntropyCrop, Pad, Rotate, Flip, RotateFlip, Skew, Transform (affine and projective builders, taper, quad distortion) |
| Colour | Grayscale, BlackWhite, Invert, Brightness, Contrast, Hue, Saturate, Lightness, Opacity, Filter(ColorMatrix), KnownFilterMatrices (including eight colour-blindness simulations), BackgroundColor |
| Filters | GaussianBlur, GaussianSharpen, BoxBlur, BokehBlur, MedianBlur, DetectEdges (10 kernels), Convolve, OilPaint, Pixelate, Vignette, Glow, Swizzle, HistogramEqualization (global, CLAHE, sliding window) |
| Thresholding | BinaryThreshold, OtsuThreshold, SauvolaThreshold, AdaptiveThreshold, NiblackThreshold, WolfJolionThreshold, PhansalkarThreshold, NickThreshold, and an auto-selecting Binarize |
| Document | Deskew, DetectSkew, DetectOrientation, AutoRotateDocument, DetectPage, CorrectPerspective, AutoCropPage, BackgroundNormalize, RemoveShadows, ContrastStretch, AutoLevels, Gamma, morphology (erode, dilate, open, close, top-hat, black-hat, thin, despeckle), connected components (RemoveSmallObjects, KeepLargestComponent, FillHoles), RemoveLines, RemoveBorders, RemoveHolePunches, SegmentTextLines, SegmentWords, NormalizeDpi, PrepareForOcr |
| Quantisation | Quantize (Wu, Octree, WebSafe, fixed palette), Dither and BinaryDither with 14 kernels |
| Compositing | DrawImage with 18 blend modes and 12 Porter-Duff alpha composition modes |
| Drawing | Rectangles, lines, polygons, ellipses, circles, DrawText and DrawLabel with an embedded bitmap font, DrawBoundingBoxes |
Pixel formats. Rgb24, Rgba32, Bgr24, Bgra32 and L8, plus the high-precision Rgb48,
Rgba64, L16, La16, La32, A8, Argb32, Abgr32 and RgbaVector. Conversions between
high-precision formats keep full precision, and 16-bit PNG and TIFF samples decode at full width.
Parallelism. Operations run row-parallel by default. For deterministic single-threaded execution:
Configuration.Default.MaxDegreeOfParallelism = 1;
Metadata
using EasyImageSharp.Metadata.Exif;
using Image<Rgba32> image = Image.Load<Rgba32>("photo.jpg");
if (image.Metadata.ExifProfile is { } exif &&
exif.TryGetValue(ExifTag.DateTimeOriginal, out var taken))
{
Console.WriteLine(taken.Value);
}
Console.WriteLine($"{image.Metadata.HorizontalResolution} DPI");
image.Mutate(ctx => ctx.AutoOrient()); // apply EXIF orientation and reset the tag
image.SaveAsJpeg("out.jpg"); // EXIF, ICC and XMP are preserved
EXIF is read and written for JPEG, PNG and TIFF, with typed access to around 60 well-known tags and lossless round-tripping of the rest. ICC and XMP profiles pass through unmodified. Resolution and per-frame metadata are preserved. EXIF orientation is never applied implicitly.
Working with untrusted input
Decoding attacker-supplied bytes is the primary attack surface of any imaging library.
var options = new DecoderOptions
{
MaxPixels = 50_000_000, // per frame; default 256 MP
MaxFrames = 32, // e.g. TIFF pages; default unlimited
};
using Image<Rgb24> image = Image.Load<Rgb24>(bytes, options);
Limits are enforced before allocation. The header is parsed and the declared size validated before
any pixel buffer is allocated, so a small file declaring enormous dimensions is rejected in microseconds.
Identify is never limited, so callers can inspect dimensions before committing to a decode.
The exception contract is closed. Framework exceptions never escape a decoder on malformed input.
| Condition | Exception |
|---|---|
| Bytes match no known format | UnknownImageFormatException |
| Malformed, truncated or internally inconsistent data | InvalidImageContentException |
Declared size exceeds DecoderOptions |
ImageSizeLimitExceededException |
| Recognised feature that is not implemented | NotSupportedException |
| Format cannot represent the image being encoded | NotSupportedException |
The first three derive from ImageFormatException, so one catch covers every kind of invalid input.
Vulnerability reports: see SECURITY.md.
Performance
BenchmarkDotNet v0.15.8, 6-core AMD Ryzen 5 4600H, .NET 10.0.11, Release.
| Operation | Input | Time | Allocated |
|---|---|---|---|
| JPEG decode | 3032×2008 → Rgba32 | 78.9 ms | 32.3 MB |
| PNG decode | 3032×2008 → Rgba32 | 131.3 ms | 27.2 MB |
| Resize, bicubic ×0.5 | 3032×2008 Rgba32 | 31.2 ms | 7.2 MB |
| Resize, bicubic ×0.5 | 3032×2008 L8 | 15.2 ms | 1.8 MB |
| Grayscale, in place | A4 at 300 DPI, L8 | 1.9 ms | 4.6 KB |
| Otsu threshold, in place | A4 at 300 DPI, L8 | 4.9 ms | 12 KB |
| Load → resize → save | 20 JPEGs | 52.6 ms each (19 img/s) | 16.5 MB |
Every row comes from a named benchmark in benchmarks/EasyImageSharp.Benchmarks, measured against a
corpus that a script rebuilds from nothing, so the table is a claim anyone can check:
python benchmarks/corpus/generate.py
dotnet run -c Release -f net10.0 --project benchmarks/EasyImageSharp.Benchmarks -- --filter "*"
dotnet run -c Release -f net10.0 --project benchmarks/EasyImageSharp.Benchmarks -- --readme-table
The last command reads the reports the run left behind and regenerates this table. Absolute milliseconds belong to the machine that produced them; the corpus, the operations measured and the Allocated column do not. What maps each row to its benchmark, and what you will and will not reproduce, is in benchmarks/README.md.
Hot paths use SIMD pixel kernels, pooled buffers and copy-on-write cloning. PNG decode is dominated by
inflating the IDAT stream rather than by the pixel code around it, and which inflater does that work
depends on the target framework: on .NET 8 it is this library's own managed DEFLATE decoder, 1.27–1.49×
faster end to end than the runtime's ZLibStream over native zlib; on .NET 10 ZLibStream is backed by
zlib-ng with hand-written SIMD, beats a managed implementation, and is kept. The decoded pixels are
identical either way, and there is nothing to configure.
AI operations
Two levels, depending on how much you want to bring yourself.
Tensor bridges — in the core package
Convert between images and tensors for any ONNX model, with no extra dependency:
using EasyImageSharp.Tensors;
// Planar [3, H, W] float tensor with ImageNet normalisation, ready for your inference session.
float[] chw = image.ToChwTensor(
channelMean: [0.485f, 0.456f, 0.406f],
channelStd: [0.229f, 0.224f, 0.225f]);
// ...and back again from a model's [3, H, W] output.
using Image<Rgb24> result = TensorImage.FromChwTensor<Rgb24>(output, width, height);
ToHwcTensor produces interleaved [H, W, 3], ToGrayscaleTensor produces [H, W] luminance, and
FromGrayscaleTensor builds an image from single-channel output. You supply the inference session; the
library handles normalisation and layout.
EasyImageSharp.AI — pre-wired models
dotnet add package EasyImageSharp.AI
using EasyImageSharp.AI;
using var ai = new ImageAiSession();
using Image<Rgb24> page = Image.Load<Rgb24>("phone-photo.jpg");
page.AutoOrient(ai); // upright the page
page.DewarpDocument(ai); // flatten curl and keystone
page.DenoiseAI(ai); // remove sensor noise
page.Mutate(ctx => ctx.Deskew().SauvolaThreshold()); // classical finish
page.SaveAsPng("clean.png");
| Operation | What it does | Why a model rather than an algorithm |
|---|---|---|
DetectOrientation / AutoOrient |
Classifies page rotation as 0°, 90°, 180° or 270° and applies a lossless correction | A projection profile is symmetric under rotation, so it cannot tell an upright page from an upside-down one. This can. |
DewarpDocument |
Flattens a photographed or curled page | A four-point perspective transform maps one plane to another; it cannot straighten a curved book spine. |
Upscale |
Learned super-resolution, tiled for large inputs | Recovers stroke topology on small glyphs that bicubic interpolation smears. |
DenoiseAI |
Residual denoiser for sensor and scan noise | Separates noise from ink, where a median filter of the same strength erodes thin strokes and serifs. |
GetSaliencyMask / RemoveBackground |
Segments the subject from its surroundings | Lets thresholding see only the document, so a cluttered desk does not pollute the statistics. |
BinarizeAI |
Learned per-pixel thresholding | Predicts a threshold per pixel instead of one window and constant, for stained or bleed-through documents. |
Each has an ...Async counterpart taking a CancellationToken, and any image-to-image ONNX model of
your own can run through the same tiling and normalisation machinery via ImageModelRunner.
Models
Published at huggingface.co/EasyImageSharp/EasyImageSharp-models, downloaded on first use and cached locally.
| Model | Operation | Size | Licence |
|---|---|---|---|
| PP-LCNet x1.0 doc-ori | AutoOrient |
6.7 MB | Apache-2.0 |
| UVDoc | DewarpDocument |
31.6 MB | MIT |
| Real-ESRGAN general x4v3 | Upscale |
4.9 MB | BSD-3-Clause |
| DnCNN blind (grayscale) | DenoiseAI |
2.7 MB | MIT |
| U²-Net | RemoveBackground (default) |
176 MB | Apache-2.0 |
| U²-Net-p | RemoveBackground (fast tier) |
4.6 MB | Apache-2.0 |
| SauvolaNet | BinarizeAI |
0.3 MB | MIT |
Weights carry their original authors' licences, which differ per file; the model repository documents each one with its input and output contract.
Supply chain
Downloading executable weights at run time is a security surface, so it is bounded:
- HTTPS only, unless explicitly overridden for a local mirror.
- SHA-256 pinned in source and verified fail-closed. A file whose hash does not match is deleted and the load throws, rather than running unverified weights. A compromised host cannot substitute a model.
- Published files are immutable. A re-export is published under a new name, so a pinned library version always resolves the exact bytes it was tested against.
- Downloads are atomic and resumable, and concurrent requests for the same model collapse into one.
- Offline mode raises
OfflineModelMissingExceptionrather than touching the network, for air-gapped deployment against a pre-seeded cache.
using var ai = new ImageAiSession(new ImageAiOptions
{
ExecutionProvider = ExecutionProvider.Auto, // CPU, CUDA, DirectML or CoreML
CachePath = "/opt/myapp/models", // default: %LOCALAPPDATA%/EasyImageSharp/models
Offline = true,
});
GPU execution requires the matching ONNX Runtime package in your application; Auto falls back to CPU
when none is present. Full details in the
package documentation.
Packages
| Package | Contents | Dependencies |
|---|---|---|
| EasyImageSharp | Codecs, Image<TPixel>, processing pipeline, document operators, drawing, metadata, pixel formats, tensor bridges |
None beyond the framework |
| EasyImageSharp.AI | ONNX-powered orientation, dewarping, super-resolution, denoising, background removal and binarisation; model hub | Microsoft.ML.OnnxRuntime |
Planned, and not in this release. A separate EasyImageSharp.Drawing package (vector geometry, a
rasteriser, brushes and pens, and text rendering from real glyph outlines), colour-space converters for
Lab, LCh, HSL, HSV and XYZ, and int8 variants of the AI models are designed but not implemented. Nothing
described above depends on them, and nothing in 1.1.0 provides them: today's drawing is the annotation
API listed in Processing, and today's colour work is the ColorMatrix family.
Dependency policy. The core package uses framework APIs only, and CI fails if it ever gains a package dependency. Free, managed, permissively-licensed dependencies may be added where they provide clear value; native binaries are confined to optional add-on packages; paid, split-licensed and copyleft dependencies are never taken.
Deployment notes
Target frameworks. .NET 8.0 and .NET 10.0. There is deliberately no netstandard target: the pixel
abstraction uses static abstract interface members, which require .NET 7 or later. Both targets are
AOT- and trimming-compatible with no conditional compilation.
Thread safety. A single Image<TPixel> instance is not thread-safe and must not be mutated
concurrently. Decoding, encoding and processing distinct images in parallel is fully supported.
Memory. Image<TPixel> owns its pixel buffer and must be disposed. After disposal, every
pixel-accessing member throws ObjectDisposedException.
Versioning. Semantic versioning. Breaking changes are confined to major releases and documented in CHANGELOG.md.
How it is verified
- Independent fixtures. Codecs are tested against a corpus of more than 500 files encoded by other
tools, each with pixel-exact ground truth alongside it, so decode paths this library's own encoders
never produce are still exercised.
check_determinism.pyregenerates the whole corpus into a scratch directory and compares decoded pixels rather than file bytes, so a real change is visible where recompression noise is not. - Reference comparisons. JPEG decoding matches a reference decoder at ≥ 61 dB PSNR; WebP output — lossy included — decodes byte-identically in the reference decoder; QOI output is byte-identical to the reference encoder.
- Hostile input. Around 150 crafted corrupt-input cases and a seeded byte-mutation fuzz pass run on every build, over seeds drawn from every fixture folder — including 15 deliberately malformed APNGs — with a deeper nightly run across three operating systems and both frameworks.
- Deployment shapes.
samples/AotSmokepublishes withPublishAotand exercises every codec, metadata path and processing stage in the published binary;samples/Thumbnailerpublishes self-contained and trimmed and must produce noIL####warning at all. - Reproducible performance. The benchmark suite and its corpus generator are in the repository, so the Performance table can be regenerated rather than taken on trust.
- Documentation that cannot drift. Every code sample in this file is transcribed into the test suite and compiled, so a rename breaks the build rather than the docs.
- Scale. 8,290 tests for the core library and 162 for the AI package, run on Ubuntu, Windows and macOS on both target frameworks, plus pack validation for both packages.
Building from source
Requires the .NET 10 SDK; the net8.0 test leg additionally requires the .NET 8 runtime.
git clone https://github.com/FarhanLodi/EasyImageSharp.git
cd EasyImageSharp
dotnet build EasyImageSharp.slnx -c Release
dotnet test EasyImageSharp.slnx -c Release
dotnet pack src/EasyImageSharp -c Release -o artifacts
The samples and the benchmark suite are deliberately outside EasyImageSharp.slnx — they single-target
net10.0, which a solution-wide net8.0 test leg cannot build — so they are run by path:
# Native AOT and trimming smoke publishes.
dotnet publish samples/AotSmoke -c Release -p:PublishAot=true
dotnet publish samples/Thumbnailer -c Release -r linux-x64 --self-contained \
-p:PublishTrimmed=true -p:TrimmerSingleWarn=false
# Benchmarks: see benchmarks/README.md.
dotnet run -c Release -f net10.0 --project benchmarks/EasyImageSharp.Benchmarks -- --filter "*" --job Dry
# Fixture corpus: regenerate, then check it by decoded pixels.
cd tests/EasyImageSharp.Tests/Fixtures && python generate.py && python check_determinism.py
Tagging vX.Y.Z runs the full suite on every OS and publishes both packages to NuGet. See
CONTRIBUTING.md for the
repository layout, coding style, fixture regeneration and how to add a codec or an operation.
Community
- Bugs and feature requests: GitHub Issues
- Security reports: SECURITY.md
- Contributing: CONTRIBUTING.md and the Code of Conduct
💖 Support
If EasyImageSharp saves you time, consider supporting its development:
- 💳 PayPal — paypal.me/FarhanLodi
- 📱 UPI (India) —
farhanlodi5@oksbi - 🏦 Bank transfer (USD) — details below
USD bank transfer details (Wise)
USD account details for Farhan Lodi on Wise. Sending from a bank in the US? Use these details for a domestic transfer. Sending from anywhere else? Make an international SWIFT transfer.
| Field | Value |
|---|---|
| Name | Farhan Lodi |
| Account type | Deposit |
| Routing number (wire and ACH) | 084009519 |
| Account number | 420927686563885 |
| SWIFT/BIC | TRWIUS35XXX |
| Bank address | Wise US Inc, 108 W 13th St, Wilmington, DE, 19801, United States |
Use the routing and account numbers when sending from the US, and the SWIFT/BIC when sending from outside the US.
📧 Need more details, a different payment method, or have a question? Email farhanlodi31@gmail.com.
📬 Contact
For work inquiries, collaboration, feature requests, or any questions, reach out to:
Farhan Lodi — farhanlodi31@gmail.com
📄 License
MIT — Copyright © 2026 Farhan Lodi.
| Product | Versions 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. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on EasyImageSharp:
| Package | Downloads |
|---|---|
|
EasyOcrSharp
High-accuracy native .NET OCR powered by EasyOCR's neural models running on ONNX Runtime. No Python required. |
|
|
PaddleOcrNet
High-accuracy native .NET OCR powered by PaddleOCR's neural models (DB detection, text-line orientation, SVTR/CRNN recognition) running on ONNX Runtime. No Python required. |
|
|
EasyImageSharp.AI
Optional ONNX Runtime add-on for EasyImageSharp: document orientation (PP-LCNet), page dewarp (UVDoc), super-resolution (Real-ESRGAN contract), learned denoise (DnCNN contract), background removal / saliency (U2-Net-p contract), learned binarisation (SauvolaNet contract) and a generic tiled image-to-image runner for your own ONNX models. Models are fetched on demand from Hugging Face with pinned SHA-256 checksums (fail-closed), cached locally, resumable, and usable fully offline; CPU by default with opt-in CUDA / DirectML / CoreML execution providers. |
|
|
LayoutSharp
Native .NET document layout analysis: detect and classify page regions (title, text, table, figure, caption, formula, header/footer…), order them the way a human reads, and optionally OCR text regions through a pluggable recognizer — all on ONNX Runtime, no Python required. |
GitHub repositories
This package is not used by any popular GitHub repositories.
1.1.0: animated PNG, BigTIFF decoding and a faster PNG path on .NET 8. Added: full APNG support — decode and encode — with dispose/blend compositing, per-frame offsets and delays, a hidden first frame and loop counts; single-frame PNG output stays byte-identical to 1.0.1. Added: BigTIFF DECODING (version 43, 8-byte offsets, 64-bit IFD entry counts, LONG8/SLONG8/IFD8), for files under 2 GiB; BigTIFF writing is not supported. Added: TiffMetadata.BigTiff, PngMetadata.IsAnimated/RepeatCount/AnimateRootFrame, PngEncoder.RepeatCount/FrameDelay, ExifDataType.Long8/SignedLong8/Ifd8 and DecoderOptions.MaxTotalPixels. Performance: a managed DEFLATE/zlib inflate is used for PNG on net8.0 ONLY, where it decodes 1.27x-1.49x faster end to end; net10.0 deliberately keeps the runtime's ZLibStream because its zlib-ng with hand-written SIMD beats a managed implementation by 1.3x-2.0x. Decoded output is byte-identical on both. Fixed: integer-overflow defects in the TIFF encoder, and decoder limit, overflow and culture hardening. Verification: the public API surface is now tracked by Microsoft.CodeAnalysis.PublicApiAnalyzers, and the Native AOT, trimming, BenchmarkDotNet and fixture-determinism checks are restored. See CHANGELOG.md.