Leith.Imaging.Quantization 287.0.0
Leith.Imaging
Leith.Imaging is a .NET 10 imaging library focused on compact raster types, colour handling, raster codecs, quantization, and terminal graphics protocols.
Features
- top-down, non-premultiplied RGBA8888 images and frame-based animations
- streaming throughout: encoded bytes on
Stream, pixels a row or a frame at a time, so peak memory follows the row rather than the image — on the expert PNG encode surface as well as the convenience one - sRGB colour contract, transfer functions, RGB/XYZ transforms, and chromatic adaptation
- PNG decoding and encoding, including APNG animation support
- a PNG document model: every ancillary chunk retained, the ones PNG3 defines parsed typed and editable, and a write that reproduces the source byte for byte
- 16-bit PNG round-tripped losslessly on an expert raster surface, additive to the RGBA8 contract
- GIF87a/GIF89a decoding and conservative GIF89a encoding
- a GIF document model with typed views over the ecosystem Application Extensions — the loop directive, an embedded ICC profile, and an XMP packet read from the bytes Adobe actually stores
- palette quantization and dithering
- separable image resampling (nearest, bilinear, Lanczos3) — see
Docs/reference/scaling/ - SIXEL encoding and decoding
- Kitty graphics and animation encoding
- framework adapters and preview controls
API
Every public operation is listed in Docs/API.md, with the conventions they all share:
Try* plus a status enum for anything malformed input can cause, Stream for encoded bytes, an
optional trailing CancellationToken observed per scanline, and TryCreate → Add* → Complete
for the streaming encoders.
A PNG can be read, resized and written — or sent to a terminal — without the image ever existing in memory:
using var wire = KittyPayloadStream.CreateForPng(terminal);
PngTranscoder.TryTranscode(source, wire, decodeLimits, encodeOptions, 800, 600, mode, ct);
wire.Complete();
Measured at about 1% of the materializing path's allocation on a 2048x2048 transcode, with gen-2 collections eliminated and no measurable time cost: Docs/reference/streaming/benchmarks.md.
Pixel contract
Rgba8888Image stores tightly packed R,G,B,A bytes in sRGB. Alpha is straight (non-premultiplied). Codecs normalize decoded colour metadata to this contract at the codec boundary.
Build
The repository targets .NET 10.
dotnet restore Leith.Imaging.CrossPlatform.slnf
dotnet build Leith.Imaging.CrossPlatform.slnf -c Release --no-restore
./scripts/run-tests.ps1
The cross-platform filter is the library, its tests, the benchmarks, the Avalonia feature gallery and
every tool except those that reference ImageSharp — tools/PngIccOracle,
tools/ApngProductOracle/ImageSharpOracle, tools/ApngEncodeOracle/ImageSharpOracle and
tools/GifCanonicalProductOracle/ImageSharpOracle — which are deliberately in no solution because
ImageSharp's targets hard-error in Release without a licence key. The full solution,
Leith.Imaging.slnx, additionally carries the Windows-only controls (Control.Wpf, Control.WinUI3,
Control.ProGpu). Build it on Windows so those do not drift unseen:
dotnet build Leith.Imaging.slnx -c Release
Testing
The gate runs locally first, and now on CI too. A commit is verified because a run on this
machine says so, so the run happens before the push, not after it — but a push to main and every
pull request are also built and gated on the self-hosted buildx64 (Linux x64) runner by
.github/workflows/ci.yml, which runs this same gate script. A push to
main additionally packs the eleven packages and publishes them to GitHub Packages. Why this
repository, which once refused CI, now has it is recorded in
Docs/decisions/ci-and-packaging.md; how to consume the feed is
in Docs/CI.md.
scripts/run-tests.ps1 is the gate. Every test project is a Microsoft.Testing.Platform application
with its own entry point; the script launches each one directly, from the list in
tests/test-hosts.json, and passes --minimum-expected-tests so a host that
discovers nothing, or that quietly loses tests, exits 9 instead of exiting 0 with nothing to show.
./scripts/run-tests.ps1 # the gate
./scripts/run-tests.ps1 -Configuration Debug -Build # build, then run every host
./scripts/run-tests.ps1 -Only Tests.Png,Tests.Gif # one or two hosts
./scripts/run-tests.ps1 -UpdateRatchet # record new counts, then read the diff
Raise the ratchet in the commit that adds the tests. Lowering one is a deliberate act that belongs in a commit message, which is the whole point of keeping the numbers in the tree.
A single host also runs on its own, which is what to reach for when bisecting a failure — and
--list-tests and --filter replace the xUnit console runner's -class and -trait there:
tests/Tests.Png/bin/Release/net10.0/Leith.Imaging.Png.Tests.exe --minimum-expected-tests 1
The ICC backend
Tests.Color.Managed always runs its parser and PNG product rows; its Little CMS differential skips
without a native library. Tests.Color.LittleCms is the oracle host and needs a native lcms2,
which this repository deliberately does not vendor. Without an engine the LittleCms host's floor is
the tests that do not need one and the managed host keeps its ordinary floor. Build the engine once:
./scripts/build-lcms2.ps1
It fetches and builds a pinned lcms2 into artifacts/native, which is gitignored. From then on
run-tests.ps1 finds it, points both ICC hosts at it and raises those hosts' floors to their real
counts — so on a machine that can run the oracle, skipping it is not a way to stay green.
Two overrides exist, and they are not the same search:
| Variable | Meaning |
|---|---|
LEITH_LCMS2_PATH |
A specific library file. Trusted if that file exists, including a versioned soname such as liblcms2.so.2.0.19. |
LEITH_LCMS2_DIR |
A directory. Auto-discovery looks only for the current platform's canonical names (lcms2.dll, liblcms2.so, liblcms2.dylib), so a WSL-built .so left in artifacts/native does not raise the Windows engine floor. |
Optional corpora
Two suites read a corpus this repository does not vendor, and skip without it. Nothing fails, so the absence is easy to miss — the gate prints the skip reason:
| Variable | Points at | Without it |
|---|---|---|
LEITH_SIXEL_CORPUS |
a directory of SIXEL files | 3 Tests.Sixel tests skip, including the fuzz case |
LEITH_GIF_CORPUS |
a directory of GIF files | Benchmarks.Gif has no inputs |
Unlike Tests.Color.LittleCms, these have no engine floor to raise, because the corpora are not
reproducible from a pinned source the way scripts/build-lcms2.ps1 rebuilds the ICC engine.
Where tests actually run
| Machine | Reach | Used for |
|---|---|---|
| This Windows box, x64 | local | the gate. Every commit is verified here and nowhere else |
| macOS 26, arm64 | ssh macos |
available, not part of the gate |
| Ubuntu, arm64 | ssh homelab |
available, not part of the gate |
| Ubuntu, x64 | WSL | available, not part of the gate |
Windows x64 is the only platform whose result gates a commit. The other three exist so a platform-specific question can be answered when one comes up — a native-binding load path, a filesystem or line-ending assumption, an arm64 floating-point difference — and answering it is a deliberate act with its result written down, not a step in the routine.
GUI backends
Only Avalonia is tested. WPF, WinUI 3 and the ProGpu bridge are switched off for the current
iteration by owner decision and come back only on an explicit request. Tests.Imaging.Wpf is out of
Leith.Imaging.slnx, so it does not build, and its exclusion carries that reason in
tests/test-hosts.json; the MVVM code-behind guard scans Control.Avalonia alone.
The bridge projects themselves still compile as part of the full solution on Windows, and the
packable-set guard still keeps Control.WinUI3 and Control.ProGpu out of what ships. Neither is a
test of a GUI backend, so neither is affected.
dotnet test
It works, and it is not the gate. global.json sets "test": { "runner": "Microsoft.Testing.Platform" }, and with that in place dotnet test Leith.Imaging.CrossPlatform.slnf -c Release runs every host and exits 0.
Two things it does not do, and they are the reasons the script exists:
- No ratchet.
dotnet testnever passes--minimum-expected-tests, so a host that discovered nothing, or that quietly lost half its tests, still reports success. - No engine. It does not point the ICC hosts at the local
lcms2, so oracle tests skip that the gate runs. The totals differ by exactly that: 4336 passing underdotnet test, 4389 underscripts/run-tests.ps1.
So dotnet test is a fine inner loop and a usable second opinion. It is not evidence that the suite
is intact, which is what a gate has to be.
Status
The streaming programme is complete and the v1 convenience surface is frozen: new formats extend the shape in Docs/API.md rather than reshaping it. Expert surfaces are added beside it when a format carries information the convenience contract cannot hold losslessly.
The high-precision PNG surface landed after that programme and did not inherit its shape at first.
Remediation has since closed it on both sides. On the encode side PngRasterEncoder writes
progressive and Adam7 datastreams a row at a time, and PngDocumentEncoder composes a re-encoded
document from those phases directly instead of staging a PNG and reparsing it: at a fixed width of
1024 an eightfold height increase costs nothing, flat where the previous implementation reached
50 MB. On the decode side both document paths are now one physical pass over the caller's stream —
a still document at 1024x2048 costs 8.4 MB where the staged decoder cost 59.7 MB — and the staging
implementation is deleted rather than merely unreachable. That was the gate before Phase 11, and it
is closed; see ROADMAP.md and
Docs/decisions/png-document-streaming-retention.md.
Format conformance is not a feature list here. It is generated from a requirement registry and the
[Conformance] markers on tests, and a claim with nothing executing behind it fails the build — so
Docs/conformance/COVERAGE.md is the honest number, currently 257 of
271 PNG/APNG and GIF requirements verified — APNG among them is complete, with one recorded
exception, GIF decode complete across all three of its dimensions, and GIF encode complete now that
canonical authoring is a surface a caller can reach. The denominator moves when a
phase writes its inventory: Phase 8 added 53 requirements in total — 51 in its design-pass
inventory and two more when compound sTER requirements were split before implementation — Phase
10's read the GIF89a playback clauses sentence by sentence and found thirteen more, Phase 11
added seven for the ICC and XMP ecosystem extensions, and Phase 12's opening design pass added
twenty-eight by reading the two CompuServe specifications for what they require of an encoder
rather than of a decoder. A share that only ever rises is measuring the
wrong thing. The programme and its remaining phases are in
Docs/conformance/.
Where a design was hard to reverse, the reasoning is written down rather than inferred from the code: Docs/decisions/ indexes the records, each opening with whether it is still live. They answer why the API is the shape it is, including the options that were rejected.
One claim is deliberately split across packages: iCCP is parsed, validated, preserved
byte-for-byte and reported by the core package, which does not claim ICC pixel fidelity.
Evaluating a profile needs a colour-management engine. Leith.Imaging.Color.Managed is that
engine for the RGB/Gray subset a PNG actually carries — packable, pure managed, no native.
Little CMS is an independent oracle under tools/LittleCms, not a shipping package. A caller
who never touches ICC keeps a managed-only dependency graph. See
Docs/decisions/icc-managed-cmm.md and
Docs/decisions/icc-littlecms-oracle-only.md.
Direction, and what is deliberately not built, are tracked in ROADMAP.md.
Working in this repository
AGENTS.md is the working agreement: the layout, the gate, the ratchet, what the
architecture guards enforce, how a conformance claim is made, and the list of things that were
decided against so they are not re-added by accident. It is written for whoever — or whatever —
opens the repository next; CLAUDE.md points at it rather than keeping a second copy.
Third-party material
Third-party code, fixtures, example assets, and licensing notes are listed in THIRD-PARTY-NOTICES.md.
License
Leith.Imaging is licensed under the MIT License. See LICENSE.
Showing the top 20 packages that depend on Leith.Imaging.Quantization.
| Packages | Downloads |
|---|---|
|
Leith.Imaging.Gif
GIF87a/GIF89a RGBA8888 codec for terminal inline graphics (dependency-free decode and encode).
|
55 |
|
Leith.Imaging.Png
PNG RGBA8888 codec for terminal inline graphics (dependency-free decode and encode).
|
55 |
|
Leith.Imaging.Sixel
Framework-agnostic SIXEL encoder and decoder for .NET.
|
47 |
|
Leith.Imaging.Gif
GIF87a/GIF89a RGBA8888 codec for terminal inline graphics (dependency-free decode and encode).
|
4 |
|
Leith.Imaging.Png
PNG RGBA8888 codec for terminal inline graphics (dependency-free decode and encode).
|
4 |
|
Leith.Imaging.Sixel
Framework-agnostic SIXEL encoder and decoder for .NET.
|
4 |
|
Leith.Imaging.Gif
GIF87a/GIF89a RGBA8888 codec for terminal inline graphics (dependency-free decode and encode).
|
0 |
|
Leith.Imaging.Png
PNG RGBA8888 codec for terminal inline graphics (dependency-free decode and encode).
|
0 |
|
Leith.Imaging.Sixel
Framework-agnostic SIXEL encoder and decoder for .NET.
|
0 |
.NET 10.0
- Leith.Imaging.Color (>= 287.0.0)
- Leith.Imaging.Model (>= 287.0.0)