From 61464ae7b59def1ea9003f989c1c3081fe493f46 Mon Sep 17 00:00:00 2001 From: lightscape-jm Date: Fri, 30 Oct 2026 00:14:21 -0600 Subject: [PATCH] fix(hide): read the preview's pixels at 8 bits a channel The Pixelate / blur window turned its preview into a bitmap from ImageMagick's "BGRA" output, which keeps the picture's own depth: a 1-bit grey or palette PNG (a flat screenshot, a diagram) gave an eighth of the bytes, and the bitmap, reading four bytes a pixel, ran past their end and could crash Filee. The pixels are now read from the picture's pixel data, 8 bits a channel whatever depth it was stored with, and their count is checked before the bitmap is made. Co-Authored-By: Claude Opus 5.5 --- src/Filee.App/Services/HideService.cs | 17 +++++++- .../Filee.App.Tests/HidePreviewPixelsTests.cs | 39 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tests/Filee.App.Tests/HidePreviewPixelsTests.cs diff --git a/src/Filee.App/Services/HideService.cs b/src/Filee.App/Services/HideService.cs index 022c07359..3fda0e3c3 100644 --- a/src/Filee.App/Services/HideService.cs +++ b/src/Filee.App/Services/HideService.cs @@ -78,7 +78,7 @@ public static Bitmap Cover(MagickImage preview, HideStyle style, double size) /// The picture as a bitmap the window draws. public static Bitmap ToBitmap(IMagickImage image) { - var pixels = image.ToByteArray(MagickFormat.Bgra); + var pixels = PixelsOf(image); var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); try { @@ -90,4 +90,19 @@ public static Bitmap ToBitmap(IMagickImage image) handle.Free(); } } + + /// + /// The picture's pixels as BGRA, four bytes each, for : read from its pixels, 8 bits a + /// channel whatever depth it was stored with. Written out as "BGRA" a 1-bit or palette PNG kept its depth, an + /// eighth of the bytes, and the bitmap read past their end. + /// + internal static byte[] PixelsOf(IMagickImage image) + { + using var pixels = image.GetPixels(); + var bytes = pixels.ToByteArray(PixelMapping.BGRA) + ?? throw new InvalidOperationException("The picture's pixels could not be read."); + if (bytes.Length != (long)image.Width * image.Height * 4) + throw new InvalidOperationException($"Expected {image.Width * image.Height * 4} bytes of pixels, got {bytes.Length}."); + return bytes; + } } diff --git a/tests/Filee.App.Tests/HidePreviewPixelsTests.cs b/tests/Filee.App.Tests/HidePreviewPixelsTests.cs new file mode 100644 index 000000000..6445d9c55 --- /dev/null +++ b/tests/Filee.App.Tests/HidePreviewPixelsTests.cs @@ -0,0 +1,39 @@ +// The Pixelate / blur window's preview of a picture stored with fewer than 8 bits a channel: a 1-bit grey or palette +// PNG (a flat screenshot, a diagram). Its pixels come out at 8 bits a channel, four bytes each, in their real colours; +// reading them as the picture's own depth gave an eighth of the bytes, and the window then read past their end. + +using Filee.App.Services; +using ImageMagick; +using ImageMagick.Drawing; + +namespace Filee.App.Tests; + +public class HidePreviewPixelsTests +{ + private static MagickImage Saved(MagickImage image) + { + var path = Path.Combine(Directory.CreateTempSubdirectory("filee-depth-").FullName, "picture.png"); + image.Write(path, MagickFormat.Png); + return new MagickImage(path); + } + + [Fact] + public void A_one_bit_and_a_palette_png_give_four_bytes_a_pixel_in_their_colours() + { + using var white = Saved(new MagickImage(MagickColors.White, 40, 30)); + using var drawing = new MagickImage(MagickColors.White, 40, 30); + new Drawables().FillColor(new MagickColor("#3E63DD")).Rectangle(0, 0, 9, 9).Draw(drawing); + using var palette = Saved(drawing); + Assert.True(white.Depth < 8 || palette.Depth < 8, "the pictures should be stored with fewer than 8 bits"); + + var whitePixels = HideService.PixelsOf(white); + var palettePixels = HideService.PixelsOf(palette); + + Assert.Equal(40 * 30 * 4, whitePixels.Length); + Assert.Equal(40 * 30 * 4, palettePixels.Length); + Assert.Equal(new byte[] { 255, 255, 255, 255 }, whitePixels[..4]); + // BGRA: the blue square in the top left corner, white beside it. + Assert.Equal(new byte[] { 0xDD, 0x63, 0x3E, 255 }, palettePixels[..4]); + Assert.Equal(new byte[] { 255, 255, 255, 255 }, palettePixels[(20 * 4)..(20 * 4 + 4)]); + } +}