Leith.Imaging.Kitty 285.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 test never 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 under dotnet test, 4389 under scripts/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.Kitty.

Packages Downloads
Leith.Term
Leith VT/xterm-class terminal engine (.NET 10). See https://github.com/buchmiet/Leith.Term.Engine.
46
Leith.Term.Kernel
Leith.Term kernel. Internal implementation of the shipping Terminal facade; not a public product API.
45
Leith.Term
Leith VT/xterm-class terminal engine (.NET 10). See https://github.com/buchmiet/Leith.Term.Engine.
3
Leith.Term
Leith VT/xterm-class terminal engine (.NET 10). See https://github.com/buchmiet/Leith.Term.Engine.
0
Leith.Term.Kernel
Leith.Term kernel. Internal implementation of the shipping Terminal facade; not a public product API.
0

.NET 10.0

Version Downloads Last updated
287.0.0 0 09/29/2026
286.0.0 45 09/29/2026
285.0.0 4 09/28/2026