ImgSmush 0.2.0
dotnet add package ImgSmush --version 0.2.0
NuGet\Install-Package ImgSmush -Version 0.2.0
<PackageReference Include="ImgSmush" Version="0.2.0" />
<PackageVersion Include="ImgSmush" Version="0.2.0" />
<PackageReference Include="ImgSmush" />
paket add ImgSmush --version 0.2.0
#r "nuget: ImgSmush, 0.2.0"
#:package ImgSmush@0.2.0
#addin nuget:?package=ImgSmush&version=0.2.0
#tool nuget:?package=ImgSmush&version=0.2.0
ImgSmush
A small, MIT-licensed, dependency-free, pure-C# image quantizer and WebP encoder. It turns 32-bit RGBA pixels into great-looking 8-bit indexed PNGs and WebP (lossy VP8 with a lossless alpha channel, or lossless VP8L), downscales in linear light, and can generate a whole responsive image set from a single original — with no native binaries, no P/Invoke, and no NuGet dependencies, so it runs anywhere .NET runs, right down to the browser via WebAssembly.
It won't out-run native libwebp or shave the last few percent off a WebP file — and that's fine. What you get instead is the same visual quality, notably small indexed PNGs, a small memory footprint, and a single managed DLL with zero strings attached: the kind of thing you can drop into a Blazor app, an AOT microservice, or a browser tab and forget about. For most projects you should reach for ImageSharp instead — see Is this the right library for you? below. This is built for the narrow slice where dependency-free portability is what you actually need.
▶ Live demo: phil-scott-78.github.io/imgsmush — the library AOT-compiled to WebAssembly, quantizing and encoding entirely in your browser (no server, no upload).
Is this the right library for you?
Probably not — and that's an honest answer, not modesty.
For most .NET image work, use ImageSharp. It's excellent: it decodes and encodes a wide range of formats, is faster, more full-featured, and far more battle-tested than this. If you want raw native speed instead, SkiaSharp and Magick.NET are the heavyweights. Any of those is the right default for the large majority of projects, and ImgSmush won't try to talk you out of them — it isn't a general-purpose imaging library. Apart from small built-in PNG and WebP readers, it has no general image decoder (for other formats you bring your own RGBA pixels).
ImgSmush exists for a narrow slice those libraries don't quite cover:
- Zero dependencies, genuinely portable. Pure BCL managed code — no native binaries, no P/Invoke. It runs identically under NativeAOT and in the browser via WebAssembly, where native-backed encoders can't follow. One small DLL, every platform.
- MIT, no strings. A permissive, clean-room implementation — no GPL, nothing to sign. (ImageSharp's current major version ships under a split license that can require a paid commercial license — a non-issue for many, a blocker for some. Worth checking for your use.)
- Lean. The quantizer and PNG encoder are allocation-light and stay off the Large Object Heap, which matters in memory-capped WASM heaps and cold-start serverless functions.
- Good at its one job. For 8-bit indexed (palette) PNGs — the pngquant-style "make this graphic small" case — its output is competitive with the best quantizers and noticeably smaller than a general-purpose full-colour PNG encoder, at matching quality.
The short version: if you need a dependency-free, MIT, AOT/WASM-friendly way to quantize images and encode WebP — especially client-side or on constrained runtimes — this is built for exactly that, and you'll be happy. If you need broad format support, maximum throughput, or you're crunching photos server-side at scale, use ImageSharp or a native library and don't look back.
Highlights
- 100% managed & portable. Pure BCL — no native binaries, no P/Invoke, no NuGet dependencies. AOT-compatible, and runs anywhere .NET runs, including the browser (WebAssembly — see the live demo above).
- Modern. Targets .NET 10 — the hot paths use the latest cross-platform SIMD.
- Lean. The quantize + indexed-PNG path is allocation-light and stays off the Large Object Heap — friendly to WebAssembly heaps and AOT/serverless cold starts.
- Indexed PNG. Standards-compliant 8-bit palette PNGs (
PLTE, plustRNSwhen the palette has transparency). - PNG reader. A small, dependency-free decoder (
Smush.DecodePng) for PNG input — every non-interlaced color type and bit depth (1–16), withtRNStransparency. - WebP, both ways. Encode lossy (VP8) with a losslessly-compressed alpha channel
or fully lossless (VP8L), and decode it back with the matching reader
(
Smush.DecodeWebp) — lossy (+alpha) and lossless still images, verified bit-exact against the libwebp reference. A rare MIT, pure-managed, zero-dependency .NET WebP codec. - Responsive sets. One call resizes + converts + smushes an original into a
series of sizes for a
<picture>element, downscaling in linear light with premultiplied alpha (correct brightness, no colour bleed) and quantizing each size from full-resolution pixels. - Great quality. Perceptual palette selection with edge-aware dithering, tuned against libimagequant as a black-box test oracle.
- MIT licensed. Permissive — ship it anywhere.
Install
dotnet add package ImgSmush
Quick start
ImgSmush works on raw RGBA pixels. It ships small PNG and WebP readers
(Smush.DecodePng, Smush.DecodeWebp) for those inputs; for any other format,
bring your own decoder (ImageSharp, SkiaSharp, System.Drawing, …) to get an RGBA buffer.
using ImgSmush;
// rgba = width * height * 4 bytes, tightly packed R,G,B,A per pixel, row-major.
// Decode a PNG or WebP with the built-in readers, or supply RGBA from any other decoder.
byte[] rgba = Smush.DecodePng(File.ReadAllBytes("in.png"), out int w, out int h);
// 1) Quantize to a palette of up to 256 colors.
SmushResult result = Smush.Quantize(rgba, w, h, new SmushOptions
{
MaxColors = 256,
DitheringLevel = 1f,
});
Console.WriteLine($"{result.ColorCount} colors, quality {result.Quality}");
// 2a) Write an indexed PNG straight from the result...
result.SavePng("out.png");
// 2b) ...or one-shot to PNG bytes.
byte[] png = Smush.ToPng(rgba, w, h);
// 3) Or encode WebP: lossy (VP8) with a lossless alpha channel...
byte[] webpLossy = Smush.ToWebp(rgba, w, h, new WebpOptions { Quality = 80 });
// ...lossless WebP (VP8L)...
byte[] webpLossless = Smush.ToWebp(rgba, w, h, new WebpOptions { Lossless = true });
// ...or a lossless WebP straight from the quantized palette (often smaller than the PNG).
byte[] webpPalette = result.EncodeWebpLossless();
Responsive image sets
Generating several sizes of the same image for a responsive <picture> element?
Hand ImgSmush the original once and let it resize → convert → smush in a single
pass. Because it still has the full-resolution original, it downscales in linear
light with premultiplied alpha (correct brightness, no colour bleed from
transparent pixels) and quantizes each size from fresh pixels instead of re-crushing
an already-quantized image.
using ImgSmush;
byte[] rgba = LoadRgbaSomehow(out int w, out int h); // bring your own decoder
ResponsiveSet set = Smush.GenerateResponsiveSet(rgba, w, h, new ResponsiveOptions
{
Format = ResponsiveFormat.WebpLossy,
Webp = new WebpOptions { Quality = 80 },
Targets =
{
ResponsiveTarget.Original("original"), // native size
ResponsiveTarget.Width("xl", 1920), // height follows aspect ratio
ResponsiveTarget.Width("lg", 1280),
ResponsiveTarget.Width("md", 768),
ResponsiveTarget.Width("sm", 320, webp: new WebpOptions { Quality = 65 }), // per-variant override
},
});
foreach (ResponsiveVariant v in set.Variants)
Console.WriteLine($"{v.Name}: {v.Width}x{v.Height} {v.Format} {v.ByteLength} bytes");
set.SaveAll("dist/img"); // writes {name}-{w}x{h}.{ext}; you build the <picture>
byte[] mdBytes = set["md"].Bytes; // or take the bytes directly
- No upscaling. A target larger than the source is produced at native size
(
IsClamped = true) or skipped (UpscalePolicy.Skip, listed inset.SkippedTargets) — so always emit each variant's actualWidth/Height. - Shared palette (on by default) keeps colours stable across breakpoints for PNG and lossless WebP; it has no effect on lossy WebP.
- Auto-tune (on by default) gives small thumbnails fewer colours at the best quality; explicit per-variant options always win.
- Metadata, not markup. The set returns bytes plus per-variant metadata; building
srcset/<picture>is intentionally left to you. - Just need a resize?
Smush.Resize(rgba, w, h, targetW, targetH, filter)returns a downscaled RGBA buffer (downscale only, in linear light).
API
| Member | Purpose |
|---|---|
Smush.DecodePng(png, out w, out h) |
Decode a non-interlaced PNG → tightly-packed RGBA bytes. |
Smush.DecodeWebp(webp, out w, out h) |
Decode a still WebP (lossy VP8 +alpha, or lossless VP8L) → tightly-packed RGBA bytes. |
Smush.Quantize(rgba, w, h, options?) |
Quantize RGBA → SmushResult. |
Smush.ToPng(rgba, w, h, options?) |
Quantize and return indexed-PNG bytes. |
Smush.ToWebp(rgba, w, h, webpOptions?) |
Encode WebP — lossy (VP8 + lossless alpha) or lossless (VP8L). |
Smush.Resize(rgba, w, h, targetW, targetH, filter?) |
Linear-light, premultiplied-alpha downscale → RGBA bytes (downscale only). |
Smush.GenerateResponsiveSet(rgba, w, h, responsiveOptions) |
Resize + convert + smush one original into many sizes → ResponsiveSet. |
SmushOptions |
MaxColors (2..256), MinQuality/MaxQuality (0..100), Speed (1 = best..10 = fastest), DitheringLevel (0..1), Gamma (0 = default). |
WebpOptions |
Lossless (bool), Quality (0..100, lossy). |
SmushResult |
Palette, IndexedPixels, ColorCount, Width, Height, Quality; EncodePng()/SavePng(path), EncodeWebpLossless()/SaveWebpLossless(path). |
ResponsiveOptions |
Targets, Format, base Quantize/Webp, Upscale, Filter, SharedPalette, AutoTune, Parallel. |
ResponsiveTarget |
Width / Height / Box / Original factories; optional density (DPR) + per-variant Quantize/Webp/Format overrides. |
ResponsiveSet |
Variants, SkippedTargets, this[name] / TryGet, SaveAll(dir, template?). |
ResponsiveVariant |
Bytes, actual Width/Height, Format, ByteLength, IsClamped, ColorCount?, Quality?, Save(path). |
ResampleFilter |
Box, Triangle, CatmullRom, Mitchell (default), Lanczos2, Lanczos3. |
ResponsiveFormat / UpscalePolicy / ResponsiveFit |
Png/WebpLossy/WebpLossless · ClampToSource/Skip · Width/Height/Box. |
SmushException |
Thrown when constraints can't be met; carries a QuantizationFailureReason (e.g. QualityBelowMinimum). |
SmushOptions.Validated() / WebpOptions.Validated() / ResponsiveOptions.Validated()
each return a copy with every field clamped to its valid range; the methods apply it
automatically, and a null options argument uses the defaults.
How it works
ImgSmush is a clean-room implementation — it ships no GPL code and has no native dependency. The quantizer (perceptual median cut, k-means refinement, vantage-point nearest-color search, edge-aware Floyd–Steinberg dithering) and the WebP encoders (VP8, VP8L, and the RIFF container) are written solely from public specifications — for WebP, RFC 6386 (VP8), the WebP Lossless Bitstream Specification (VP8L), and the WebP Container Specification.
Output is validated by differential testing: libimagequant and ImageSharp / libwebp are used only as black-box decode/quality oracles in the test suite, never as a source. Lossless and alpha round-trip exactly; lossy decodes to high PSNR in an independent decoder.
License
ImgSmush is licensed under the MIT license — see LICENSE.
Building from source
Prerequisites: the .NET 10 SDK.
dotnet build ImgSmush.slnx -c Debug
dotnet test ImgSmush.slnx -c Debug
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.