ImgSmush 0.2.0

dotnet add package ImgSmush --version 0.2.0
                    
NuGet\Install-Package ImgSmush -Version 0.2.0
                    
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="ImgSmush" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ImgSmush" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="ImgSmush" />
                    
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 ImgSmush --version 0.2.0
                    
#r "nuget: ImgSmush, 0.2.0"
                    
#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 ImgSmush@0.2.0
                    
#: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=ImgSmush&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=ImgSmush&version=0.2.0
                    
Install as a Cake Tool

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, plus tRNS when 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), with tRNS transparency.
  • 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 in set.SkippedTargets) — so always emit each variant's actual Width/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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
0.2.0 106 6/4/2026
0.1.0 103 6/3/2026