Replace SixLabors.ImageSharp with a self-contained image decoder - #358
Merged
Conversation
ImageSharp moved to a split licence at 4.0, which broke the build for anyone letting the version float (#230) and left this package pinned to 3.1.x indefinitely. Issue #354 asked whether the dependency could be replaced with something more permissive, or with code that only does what this library needs. This is that code: ktsu.ImGui.App now has no imaging dependency at all. ImGui.App/Images/ decodes to ImagePixels, a tightly packed straight-alpha RGBA8 buffer, which is exactly what UploadTextureRGBA wants: - PNG: all five colour types, bit depths 1 to 16, palettes, tRNS in all three forms, Adam7 interlacing, all five scanline filters - JPEG: baseline, extended sequential and progressive Huffman, any sampling factors, restart intervals, greyscale and colour - BMP: core and info headers of every version, 1/4/8/16/24/32 bit, top-down and bottom-up, BI_RGB and BI_BITFIELDS - TGA: colour-mapped, true-colour and greyscale, raw and run-length encoded, 8/15/16/24/32 bit The format is chosen from the file's own bytes rather than its extension, and anything unrecognised or malformed raises InvalidImageDataException naming what was found. Inflation uses ZLibStream from the base class library, so nothing new is pulled in. ImageResampler replaces the Welch resize behind SetWindowIcon: separable Lanczos-3 whose support widens by the reduction factor when downscaling, run on premultiplied alpha so transparent pixels do not bleed colour into their neighbours. SetWindowIcon reduces a large source once before deriving its ten sizes rather than filtering the full image ten times. Verified against libjpeg and libpng through Pillow: PNG, BMP and TGA decode bit-exactly, and JPEG lands within three units per channel across 4:4:4, 4:2:2, 4:2:0, greyscale, progressive and restart-marker codings — the range reference decoders differ by among themselves. BREAKING: ImGuiApp.UseImageBytes now takes ImagePixels rather than ImageSharp's Image<Rgba32>. That type was the only place the dependency reached the public surface, so removing it cannot be done compatibly. Callers passing an Image<Rgba32> should load through ImageDecoder instead. Also not carried over: GIF, WebP and TIFF, which ImageSharp supported and this does not; run-length encoded BMP; and arithmetic-coded, lossless or CMYK JPEG. None appear in the icons and textures this library loads, and each is rejected with a message saying so rather than silently. Closes #354 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP
…-354-7ftjju #355 restructured ImGui.App/README.md, moving Versioning, Support and Acknowledgments above Contributing and dropping the duplicate copy at the end of the file. The conflict was that duplicate: this branch had edited its Acknowledgments list to drop ImageSharp, and main deleted the block outright. Took main's structure and dropped the ImageSharp entry from the surviving Acknowledgments list instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP
Neither can misbehave as written — the IDCT's (2x+1)*u peaks at 105 for an 8x8 block, and a GUID-and-extension filename is never rooted, so nothing overflows and no earlier argument is dropped. Both fixes are free, though, and an integer product feeding a cosine is worth not writing regardless. - InverseDct.BuildBasis computes the cosine argument in double from the first operand. - The extension-sniffing test joins its temp path with Path.Join. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP
The three new null guards used ArgumentNullException.ThrowIfNull, which the SDK analyzer rejects because it is .NET 6 and later only; Polyfill's Ensure.NotNull is what the rest of ImGui.App uses and what compiles on every target. The iOS job caught it — its Roslyn is new enough to load the SDK analyzers, which the container this was written in is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP
…-354-7ftjju #356 and #357 landed: ImGuiNodeEditor was renamed to ImGui.NodeEditor, and tokenizing was split out of ImGui.SyntaxHighlighting into its own SyntaxHighlighting library. Neither touches the image decoder. The only conflict was CLAUDE.md's Libraries list, where main rewrote both the ImGui.App and ImGui.Widgets entries. Took main's text for both and re-applied just this branch's addition: the sentence on ImGui.App pointing at the self-contained image decoding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP
|
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #354.
The answer to the question
Yes to the second option.
ktsu.ImGui.Appnow decodes images itself and has no imaging dependency at all.The dependency was only ever doing three things: decode a file to RGBA8, crop, and scale. ImageSharp moved to a split licence at 4.0, which broke the build for anyone letting the version float (#230) and left the package pinned to 3.1.x indefinitely — a pin nobody can lift. Swapping in another package (StbImageSharp, ImageMagick bindings) trades one licence and one supply-chain surface for another; writing the ~600 lines of decoding this library actually uses removes the question. The repo already has the precedent:
ImGui.App.Testing/Bitmap32.cshand-rolls PNG encoding overZLibStreamfor exactly this reason.What's here
ImGui.App/Images/decodes toImagePixels— a tightly packed, straight-alpha RGBA8 buffer with no row padding, which is exactly whatUploadTextureRGBAwants.tRNSin all three forms, Adam7 interlacing, all five scanline filtersBI_RGBandBI_BITFIELDSBI_RLE4/BI_RLE8, embedded PNG/JPEG payloadsThe format comes from the file's own leading bytes, never its extension, so a mislabelled file still loads. Anything unrecognised or malformed raises
InvalidImageDataExceptionnaming what was found. Inflation usesSystem.IO.Compression.ZLibStreamfrom the BCL, so nothing new is pulled in.ImageResamplerreplaces the Welch resize behindSetWindowIcon: a separable Lanczos-3 filter whose support widens by the reduction factor when downscaling, so shrinking averages rather than point-samples. It runs on premultiplied alpha and unpremultiplies afterwards, so the colour of fully transparent pixels does not bleed into their visible neighbours — which the old path did not do.Verification
Decoder output was compared against libpng/libjpeg (through Pillow) over a matrix of encodings:
tRNS, Adam7 interlaced: bit-exact, all of them.BI_RGBBMP's alpha is honoured, with an all-zero channel treated as opaque, matching stb_image. Pillow discards it. Checked against the original RGBA source, our output is bit-exact and Pillow's is the lossy one.)New tests live in
tests/ImGui.App.Tests/Images/(76 cases).TestImageBuilderencodes PNG, BMP and TGA in memory so the decoders can be driven over their whole feature matrix — every colour type, every bit depth, every scanline filter, both row orders, both TGA packet kinds — without checking binary fixtures into the repo. The JPEG cases, which need a real encoder, are small base64 constants.CI on
78dde73: 1563 tests, 0 failures, all 18 test projects green across Linux, Windows and macOS, plus a green iOS build. That run is the one that matters most here: the six demo UI suites decode the repository's real PNGs through the actual render path, so the new decoder is exercised againsticon.png,ktsu.pngand thetrevor-*.pngset rather than only against synthetic fixtures. SonarQube's quality gate passed at 83.9% coverage on new code and 0% duplication.Performance
Measured on a 2048×2048 image:
SetWindowIconicon setPNG got faster than ImageSharp's path here because scanline expansion writes straight into the destination span with a memcpy fast path for 8-bit truecolour. JPEG is slower than libjpeg (floating-point IDCT, no SIMD) but well inside what a texture load can absorb; a DC-only block fast path covers the flat regions that dominate real photographs.
Breaking change
ImGuiApp.UseImageBytes(Image<Rgba32>, Action<byte[]>)is nowUseImageBytes(ImagePixels, Action<byte[]>). That parameter was the only place the dependency reached the public surface, so removing the dependency cannot be done compatibly. Callers passing anImage<Rgba32>should load throughImageDecoderinstead. Nothing else in the public API changed, andGetOrLoadTexture,TryGetTexture,SetWindowIconand the texture cache are untouched from a caller's point of view.Also worth stating plainly: GIF, WebP and TIFF are gone. ImageSharp decoded them and this does not. They do not appear in any icon or texture in this repo or its examples, and a file in one of those formats now fails with a message that names the format rather than silently — but if anyone is loading them, this is the regression to know about.
Docs
CLAUDE.mdgains an "Image decoding" section with the support matrix and the three behaviours worth knowing before changing any of it (16-bit truncation, the all-zero-alpha heuristic, and JPEG's whole-image coefficient buffer).ImGui.App/README.mdgains an "Image Loading" section with the same matrix and a standalone usage example. ImageSharp is removed from every dependency list.🤖 Generated with Claude Code
https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP