From 41ff5a0f25011a585eabbc4f0e87507ecc93c72c Mon Sep 17 00:00:00 2001 From: Sawraz Date: Thu, 9 Jul 2026 15:17:40 +0600 Subject: [PATCH 1/2] DW-40: carry original BitsPerPixel through GetAllFrames/Clone/RotateFlip/Redact and preserve depth on resize --- .../Data/DW-40 MixedDepthMultiPage.tif | Bin 0 -> 1456 bytes .../UnitTests/AnyBitmapFunctionality.cs | 74 ++++++++++++ .../IronSoftware.Drawing.Common/AnyBitmap.cs | 108 ++++++++++++++---- 3 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/DW-40 MixedDepthMultiPage.tif diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/DW-40 MixedDepthMultiPage.tif b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/DW-40 MixedDepthMultiPage.tif new file mode 100644 index 0000000000000000000000000000000000000000..96846cc0d67177fdaf2e72cdf76ca720f7b0e2ff GIT binary patch literal 1456 zcmah}*HXhk47_vi1#q0=K>a!90e^kbzW?XU> z5GRPU6@|GZE2^d&re!;>=Mxw-vq9J(2jKxe3-Az?67XnOIfqgGpaPDn=aw)o+o<5U z=&FX3yvZG$ik=ai5tuZy$-F^M=EuY88z9n<`di=(sV~Na)nBRm>f<+o=1vx;XXh7} zSJyYU%e&S6!y}!JP&gXJZ^Mo;PV2Iv``Zh!S`O{d@6OtyZa?&#|s=+D$WJ+CV0 zFVtP#%H;G{>b_1P3;G*%XP53J{hhkEV@wtOgSxwYsx|$Sy1&%~JNg&hjuwlhi2hBt yXWx+x`VZZ%Jp(uCzx~@M!-D+5mb7i31i9+lw*pXYpE%R@?PHL@+Xpme f.BitsPerPixel)); + } + + [FactWithAutomaticDisplayName] + public void DW_40_CloneRectangle_ShouldPreserveOriginalBitsPerPixel() + { + var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); + + var cropped = bitmap.Clone(new Rectangle(0, 0, 100, 100)); + + Assert.Equal(1, cropped.BitsPerPixel); + } + + [FactWithAutomaticDisplayName] + public void DW_40_RotateFlip_ShouldPreserveOriginalBitsPerPixel() + { + var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); + + var rotated = bitmap.RotateFlip(AnyBitmap.RotateMode.Rotate90, AnyBitmap.FlipMode.None); + + Assert.Equal(1, rotated.BitsPerPixel); + } + + [FactWithAutomaticDisplayName] + public void DW_40_Redact_ShouldPreserveOriginalBitsPerPixel() + { + var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); + + var redacted = bitmap.Redact(new Rectangle(0, 0, 10, 10), Color.Black); + + Assert.Equal(1, redacted.BitsPerPixel); + } + + [FactWithAutomaticDisplayName] + public void DW_40_Resize_ShouldReportHonestDepthForSubEightBppSource() + { + var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); + Assert.Equal(1, bitmap.BitsPerPixel); + + var resized = new AnyBitmap(bitmap, 50, 50); + + Assert.Equal(32, resized.BitsPerPixel); + } + + [FactWithAutomaticDisplayName] + public void DW_40_Resize_ShouldPreserveDepthForRepresentableFormats() + { + var rgb24 = AnyBitmap.FromFile(GetRelativeFilePath("24_bit.png")); + Assert.Equal(24, rgb24.BitsPerPixel); + Assert.Equal(24, new AnyBitmap(rgb24, 20, 20).BitsPerPixel); + + string path64 = "dw40_tmp64.png"; + using (var img64 = new Image(30, 30)) { img64.SaveAsPng(path64); } + try + { + var rgba64 = AnyBitmap.FromFile(path64); + Assert.Equal(64, rgba64.BitsPerPixel); + Assert.Equal(64, new AnyBitmap(rgba64, 15, 15).BitsPerPixel); + } + finally + { + CleanResultFile(path64); + } + } + [TheoryWithAutomaticDisplayName()] [InlineData("mountainclimbers.jpg", "image/jpeg", AnyBitmap.ImageFormat.Jpeg)] [InlineData("watermark.deployment.png", "image/png", AnyBitmap.ImageFormat.Png)] diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index dea6ec0..ebad0fe 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -241,7 +241,11 @@ public AnyBitmap Clone() public AnyBitmap Clone(Rectangle rectangle) { var cloned = GetInternalImages().Select(img => img.Clone(x => x.Crop(rectangle))); - return new AnyBitmap(cloned); + var result = new AnyBitmap(cloned); + // Cropping preserves the source color depth, so carry the original depth over (otherwise + // it would fall back to the decoded 32bpp value). + result._originalBitsPerPixel = _originalBitsPerPixel; + return result; } /// @@ -711,9 +715,15 @@ public AnyBitmap(Stream stream, bool preserveOriginalFormat) } /// - /// + /// Creates a new by resizing to the + /// given dimensions. + /// Color depth: resizing resamples (blends) pixels, so + /// of the result reflects the resized image and may differ from the source. In particular a + /// source whose original depth is below 8bpp (e.g. a 1bpp black & white image) cannot exist + /// below 8bpp in memory, so the resized result reports its actual decoded depth rather than the + /// original low depth. The depth of 8/24/32/64bpp sources is otherwise preserved. /// - /// The from which to + /// The from which to /// create the new . /// The width of the new AnyBitmap. /// The height of the new AnyBitmap. @@ -1055,6 +1065,14 @@ public static AnyBitmap LoadAnyBitmapFromRGBBuffer(byte[] buffer, int width, int /// private int? _originalBitsPerPixel = null; + /// + /// The original color depth (bits per pixel) of each frame of a multi-page TIFF, in the same + /// order as the decoded frames. Populated while decoding the TIFF so that + /// can report each frame's true source depth (pages of a TIFF may differ in depth). Remains + /// null for non-TIFF sources. + /// + private IReadOnlyList _framesOriginalBitsPerPixel = null; + //cache of the bits per pixel of the in-memory (decoded) image private int InMemoryBitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel; @@ -1150,14 +1168,26 @@ public IEnumerable GetAllFrames get { var images = GetInternalImages(); - if (images.Count == 1) + IEnumerable frames = images.Count == 1 + ? ImageFrameCollectionToImages(images[0].Frames).Select(x => (AnyBitmap)x) + : images.Select(x => (AnyBitmap)x); + + // Each frame is a freshly-cast AnyBitmap. Carry the captured original source depth + // onto each frame. Only do this when this object captured an original depth (i.e. a + // TIFF loaded preserving its original format); otherwise leave frames as-is. + if (_originalBitsPerPixel == null) { - return ImageFrameCollectionToImages(images[0].Frames).Select(x => (AnyBitmap)x); + return frames; } - else + + IReadOnlyList perFrame = _framesOriginalBitsPerPixel; + return frames.Select((frame, index) => { - return images.Select(x => (AnyBitmap)x); - } + frame._originalBitsPerPixel = perFrame != null && index < perFrame.Count + ? perFrame[index] + : _originalBitsPerPixel; + return frame; + }); } } @@ -1440,7 +1470,11 @@ public static AnyBitmap RotateFlip( image.Mutate(x => x.RotateFlip(rotateModeImgSharp, flipModeImgSharp)); - return new AnyBitmap(image); + var result = new AnyBitmap(image); + // Rotating/flipping preserves the source color depth, so carry the original depth over + // (otherwise it would fall back to the decoded 32bpp value). + result._originalBitsPerPixel = bitmap._originalBitsPerPixel; + return result; } /// @@ -1476,8 +1510,12 @@ public static AnyBitmap Redact( Rectangle rectangle = Rectangle; var brush = new SolidBrush(color); image.Mutate(ctx => ctx.Fill(brush, rectangle)); - - return new AnyBitmap(image); + + var result = new AnyBitmap(image); + // Redacting a region preserves the source color depth, so carry the original depth over + // (otherwise it would fall back to the decoded 32bpp value). + result._originalBitsPerPixel = bitmap._originalBitsPerPixel; + return result; } /// @@ -3024,18 +3062,12 @@ public override void WarningHandlerExt(Tiff tif, object clientData, string metho using var tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()); if (tiff == null) return (1, null); // Default to single frame if can't read - // Read frame-0 fields before NumberOfDirectories(), which may move the active directory. - FieldValue[] bitsPerSampleField = tiff.GetField(TiffTag.BITSPERSAMPLE); - FieldValue[] samplesPerPixelField = tiff.GetField(TiffTag.SAMPLESPERPIXEL); - - // BitsPerSample defaults to 1 and SamplesPerPixel defaults to 1 per the TIFF spec. - int bitsPerSample = bitsPerSampleField != null ? bitsPerSampleField[0].ToInt() : 1; - int samplesPerPixel = samplesPerPixelField != null ? samplesPerPixelField[0].ToInt() : 1; - int bitsPerPixel = bitsPerSample * samplesPerPixel; + // Read frame-0 depth before NumberOfDirectories(), which may move the active directory. + int? bitsPerPixel = ReadDirectoryBitsPerPixel(tiff); int frameCount = tiff.NumberOfDirectories(); - return (frameCount, bitsPerPixel > 0 ? bitsPerPixel : (int?)null); + return (frameCount, bitsPerPixel); } catch { @@ -3043,6 +3075,24 @@ public override void WarningHandlerExt(Tiff tif, object clientData, string metho } } + /// + /// Reads the original bits per pixel (BitsPerSample x SamplesPerPixel) of the TIFF directory + /// that is currently active on . + /// + /// The bits per pixel, or null if it cannot be determined. + private static int? ReadDirectoryBitsPerPixel(Tiff tiff) + { + FieldValue[] bitsPerSampleField = tiff.GetField(TiffTag.BITSPERSAMPLE); + FieldValue[] samplesPerPixelField = tiff.GetField(TiffTag.SAMPLESPERPIXEL); + + // BitsPerSample defaults to 1 and SamplesPerPixel defaults to 1 per the TIFF spec. + int bitsPerSample = bitsPerSampleField != null ? bitsPerSampleField[0].ToInt() : 1; + int samplesPerPixel = samplesPerPixelField != null ? samplesPerPixelField[0].ToInt() : 1; + int bitsPerPixel = bitsPerSample * samplesPerPixel; + + return bitsPerPixel > 0 ? bitsPerPixel : (int?)null; + } + private Lazy> OpenTiffToImageSharp() { return new Lazy>(() => @@ -3105,6 +3155,9 @@ private List ReadTiffFrames(Tiff tiff) SetTiffCompression(tiff); List images = new(); + // Original source depth per decoded frame, kept in the same order as 'images' (thumbnails + // are skipped in both) so GetAllFrames can report each frame's true source depth. + List framesBitsPerPixel = new(); int index = 0; do @@ -3125,6 +3178,9 @@ private List ReadTiffFrames(Tiff tiff) "Split this page into smaller images before loading."); } + // Capture this page's original depth before ReadRGBAImage decodes it to 32bpp. + int? frameBitsPerPixel = ReadDirectoryBitsPerPixel(tiff); + // Read the image into the memory buffer int[] raster = new int[height * width]; if (!tiff.ReadRGBAImage(width, height, raster)) @@ -3139,6 +3195,7 @@ private List ReadTiffFrames(Tiff tiff) image.Metadata.HorizontalResolution = horizontalResolution; image.Metadata.VerticalResolution = verticalResolution; images.Add(image); + framesBitsPerPixel.Add(frameBitsPerPixel); //Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff //Note2: 'yield return' make it super slow @@ -3148,6 +3205,7 @@ private List ReadTiffFrames(Tiff tiff) } while (tiff.ReadDirectory()); + _framesOriginalBitsPerPixel = framesBitsPerPixel; return images; } @@ -3617,14 +3675,14 @@ private void SetPixelColor(int x, int y, Color color) private void LoadAndResizeImage(AnyBitmap original, int width, int height) { - //this prevent case when original is changed before Lazy is loaded - Binary = original.Binary; + // Capture the source's decoded image up front so we are independent of the original's lifetime. + Image sourceImage = original.GetFirstInternalImage(); _lazyImage = new Lazy>(() => { - - var image = Image.Load(Binary); - image.Mutate(img => img.Resize(width, height)); + // Resize a clone of the already-decoded image so its pixel type (and therefore its + // color depth) is preserved. + var image = sourceImage.Clone(img => img.Resize(width, height)); //update Binary using var memoryStream = new MemoryStream(); From 4c66813d2d0597416918c239895a9a011f050512 Mon Sep 17 00:00:00 2001 From: Sawraz Date: Fri, 17 Jul 2026 14:20:04 +0600 Subject: [PATCH 2/2] DW-40: preserve multi-page frames on resize, carry per-frame depth in Clone, add Image/originalBpp ctor, clarify Redact rationale --- .../UnitTests/AnyBitmapFunctionality.cs | 50 +++++++++++---- .../IronSoftware.Drawing.Common/AnyBitmap.cs | 61 ++++++++++++------- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs index 6626a3b..979f520 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs @@ -977,7 +977,7 @@ public void Should_Return_BitsPerPixel() [InlineData("ScanDev_BW.tif", 1)] [InlineData("ScanDev_Gray.tif", 8)] [InlineData("ScanDev_Color.tif", 24)] - public void DW_9_LoadImage_ShouldReturnOriginalBitsPerPixel(string fileName, int expectedBitsPerPixel) + public void LoadImage_ShouldReturnOriginalBitsPerPixel(string fileName, int expectedBitsPerPixel) { string imagePath = GetRelativeFilePath(fileName); @@ -987,7 +987,7 @@ public void DW_9_LoadImage_ShouldReturnOriginalBitsPerPixel(string fileName, int } [IgnoreOnUnixFact] - public void DW_9_LoadBlackAndWhiteTiff_ShouldReturnOriginalBitsPerPixel_AndAllowChangingBpp() + public void LoadBlackAndWhiteTiff_ShouldReturnOriginalBitsPerPixel_AndAllowChangingBpp() { string imagePath = GetRelativeFilePath("tifimg.tif"); @@ -1004,7 +1004,7 @@ public void DW_9_LoadBlackAndWhiteTiff_ShouldReturnOriginalBitsPerPixel_AndAllow } [FactWithAutomaticDisplayName] - public void DW_9_LoadImage_NotPreservingOriginalFormat_ShouldReturn32BitsPerPixel() + public void LoadImage_NotPreservingOriginalFormat_ShouldReturn32BitsPerPixel() { string imagePath = GetRelativeFilePath("ScanDev_BW.tif"); @@ -1017,7 +1017,7 @@ public void DW_9_LoadImage_NotPreservingOriginalFormat_ShouldReturn32BitsPerPixe [InlineData(8)] [InlineData(24)] [InlineData(32)] - public void DW_9_ChangeBitsPerPixel_ShouldReturnRequestedColorDepth(int targetBitsPerPixel) + public void ChangeBitsPerPixel_ShouldReturnRequestedColorDepth(int targetBitsPerPixel) { string imagePath = GetRelativeFilePath("ScanDev_BW.tif"); var bitmap = AnyBitmap.FromFile(imagePath); @@ -1030,7 +1030,7 @@ public void DW_9_ChangeBitsPerPixel_ShouldReturnRequestedColorDepth(int targetBi } [FactWithAutomaticDisplayName] - public void DW_9_ChangeBitsPerPixel_WithUnsupportedDepth_ShouldThrow() + public void ChangeBitsPerPixel_WithUnsupportedDepth_ShouldThrow() { string imagePath = GetRelativeFilePath("ScanDev_BW.tif"); var bitmap = AnyBitmap.FromFile(imagePath); @@ -1039,7 +1039,7 @@ public void DW_9_ChangeBitsPerPixel_WithUnsupportedDepth_ShouldThrow() } [FactWithAutomaticDisplayName] - public void DW_9_BitsPerPixel_IsIntentionallyDecoupledFromStrideAndScan0() + public void BitsPerPixel_IsIntentionallyDecoupledFromStrideAndScan0() { // A 1bpp source is decoded into a 32bpp buffer in memory. BitsPerPixel reports the // original depth (1), but Stride and Scan0 must describe the decoded 32bpp buffer. @@ -1059,7 +1059,7 @@ public void DW_9_BitsPerPixel_IsIntentionallyDecoupledFromStrideAndScan0() } [FactWithAutomaticDisplayName] - public void DW_9_ChangeBitsPerPixel_DurabilityDependsOnEncoder() + public void ChangeBitsPerPixel_DurabilityDependsOnEncoder() { string imagePath = GetRelativeFilePath("ScanDev_BW.tif"); var converted = AnyBitmap.FromFile(imagePath).ChangeBitsPerPixel(8); @@ -1080,7 +1080,7 @@ public void DW_9_ChangeBitsPerPixel_DurabilityDependsOnEncoder() } [FactWithAutomaticDisplayName] - public void DW_40_GetAllFrames_ShouldReturnOriginalBitsPerPixelPerFrame() + public void GetAllFrames_ShouldReturnOriginalBitsPerPixelPerFrame() { var singleFrame = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")).GetAllFrames.ToList(); Assert.Single(singleFrame); @@ -1092,7 +1092,7 @@ public void DW_40_GetAllFrames_ShouldReturnOriginalBitsPerPixelPerFrame() } [FactWithAutomaticDisplayName] - public void DW_40_CloneRectangle_ShouldPreserveOriginalBitsPerPixel() + public void CloneRectangle_ShouldPreserveOriginalBitsPerPixel() { var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); @@ -1102,7 +1102,7 @@ public void DW_40_CloneRectangle_ShouldPreserveOriginalBitsPerPixel() } [FactWithAutomaticDisplayName] - public void DW_40_RotateFlip_ShouldPreserveOriginalBitsPerPixel() + public void RotateFlip_ShouldPreserveOriginalBitsPerPixel() { var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); @@ -1112,7 +1112,7 @@ public void DW_40_RotateFlip_ShouldPreserveOriginalBitsPerPixel() } [FactWithAutomaticDisplayName] - public void DW_40_Redact_ShouldPreserveOriginalBitsPerPixel() + public void Redact_ShouldPreserveOriginalBitsPerPixel() { var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); @@ -1122,7 +1122,7 @@ public void DW_40_Redact_ShouldPreserveOriginalBitsPerPixel() } [FactWithAutomaticDisplayName] - public void DW_40_Resize_ShouldReportHonestDepthForSubEightBppSource() + public void Resize_ShouldReportHonestDepthForSubEightBppSource() { var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")); Assert.Equal(1, bitmap.BitsPerPixel); @@ -1133,7 +1133,7 @@ public void DW_40_Resize_ShouldReportHonestDepthForSubEightBppSource() } [FactWithAutomaticDisplayName] - public void DW_40_Resize_ShouldPreserveDepthForRepresentableFormats() + public void Resize_ShouldPreserveDepthForRepresentableFormats() { var rgb24 = AnyBitmap.FromFile(GetRelativeFilePath("24_bit.png")); Assert.Equal(24, rgb24.BitsPerPixel); @@ -1153,6 +1153,30 @@ public void DW_40_Resize_ShouldPreserveDepthForRepresentableFormats() } } + [FactWithAutomaticDisplayName] + public void Resize_ShouldPreserveFrameCountForMultiPageTiff() + { + // Resizing a multi-page TIFF resizes every page, so the frame count is preserved. + var multiPage = AnyBitmap.FromFile(GetRelativeFilePath("DW-40 MixedDepthMultiPage.tif")); + Assert.Equal(3, multiPage.FrameCount); + + var resized = new AnyBitmap(multiPage, 8, 8); + + Assert.Equal(3, resized.FrameCount); + Assert.Equal(3, resized.GetAllFrames.Count()); + } + + [FactWithAutomaticDisplayName] + public void CloneRectangle_ShouldPreservePerFrameDepthForMultiPageTiff() + { + // Cropping keeps frames 1:1, so each cropped frame still reports its own source depth. + var multiPage = AnyBitmap.FromFile(GetRelativeFilePath("DW-40 MixedDepthMultiPage.tif")); + + var cropped = multiPage.Clone(new Rectangle(0, 0, 10, 10)); + + Assert.Equal(new[] { 1, 8, 24 }, cropped.GetAllFrames.Select(f => f.BitsPerPixel)); + } + [TheoryWithAutomaticDisplayName()] [InlineData("mountainclimbers.jpg", "image/jpeg", AnyBitmap.ImageFormat.Jpeg)] [InlineData("watermark.deployment.png", "image/png", AnyBitmap.ImageFormat.Png)] diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index ebad0fe..70c4dc3 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -242,9 +242,10 @@ public AnyBitmap Clone(Rectangle rectangle) { var cloned = GetInternalImages().Select(img => img.Clone(x => x.Crop(rectangle))); var result = new AnyBitmap(cloned); - // Cropping preserves the source color depth, so carry the original depth over (otherwise - // it would fall back to the decoded 32bpp value). + // Cropping is lossless (it only removes pixels) and keeps frames 1:1 with GetInternalImages, + // so carry both the scalar and the per-frame source depths over. result._originalBitsPerPixel = _originalBitsPerPixel; + result._framesOriginalBitsPerPixel = _framesOriginalBitsPerPixel; return result; } @@ -722,6 +723,8 @@ public AnyBitmap(Stream stream, bool preserveOriginalFormat) /// source whose original depth is below 8bpp (e.g. a 1bpp black & white image) cannot exist /// below 8bpp in memory, so the resized result reports its actual decoded depth rather than the /// original low depth. The depth of 8/24/32/64bpp sources is otherwise preserved. + /// Multi-page sources: every page/frame is resized, so a multi-page TIFF keeps its + /// frame count. /// /// The from which to /// create the new . @@ -846,6 +849,19 @@ internal AnyBitmap(Image image) : this([image]) { } + /// + /// Wraps an already-decoded image while carrying over the original source color depth. + /// Used by derived operations (RotateFlip, Redact) so the cast operator + /// stays free of any assumed original depth. + /// + /// The decoded image to wrap. + /// The original source color depth to report from + /// , or null to report the in-memory decoded depth. + private AnyBitmap(Image image, int? originalBitsPerPixel) : this(image) + { + _originalBitsPerPixel = originalBitsPerPixel; + } + /// /// Note: This only use for Casting It won't create new object Image /// @@ -1470,11 +1486,9 @@ public static AnyBitmap RotateFlip( image.Mutate(x => x.RotateFlip(rotateModeImgSharp, flipModeImgSharp)); - var result = new AnyBitmap(image); - // Rotating/flipping preserves the source color depth, so carry the original depth over - // (otherwise it would fall back to the decoded 32bpp value). - result._originalBitsPerPixel = bitmap._originalBitsPerPixel; - return result; + // Rotating/flipping is lossless (it only moves pixels), so the source color depth is + // carried over. + return new AnyBitmap(image, bitmap._originalBitsPerPixel); } /// @@ -1511,11 +1525,12 @@ public static AnyBitmap Redact( var brush = new SolidBrush(color); image.Mutate(ctx => ctx.Fill(brush, rectangle)); - var result = new AnyBitmap(image); - // Redacting a region preserves the source color depth, so carry the original depth over - // (otherwise it would fall back to the decoded 32bpp value). - result._originalBitsPerPixel = bitmap._originalBitsPerPixel; - return result; + // Redact fills a region but leaves the rest of the image untouched, so it carries the + // source's declared color depth as the (decoupled, in-memory) BitsPerPixel label, + // consistent with the loaded image. This is a label only: if the redaction color cannot + // exist at that depth it is not literally representable, and the label is dropped on + // re-encode. (Unlike resize, which resamples the whole image and is reported honestly.) + return new AnyBitmap(image, bitmap._originalBitsPerPixel); } /// @@ -3675,21 +3690,25 @@ private void SetPixelColor(int x, int y, Color color) private void LoadAndResizeImage(AnyBitmap original, int width, int height) { - // Capture the source's decoded image up front so we are independent of the original's lifetime. - Image sourceImage = original.GetFirstInternalImage(); + // Capture the source's decoded images up front so we are independent of the original's + // lifetime. Every page/frame is resized so multi-page TIFFs keep their frame count. + IReadOnlyList sourceImages = original.GetInternalImages(); _lazyImage = new Lazy>(() => { - // Resize a clone of the already-decoded image so its pixel type (and therefore its + // Resize a clone of each page/frame so the source pixel type (and therefore its // color depth) is preserved. - var image = sourceImage.Clone(img => img.Resize(width, height)); + var resized = sourceImages + .Select(img => img.Clone(c => c.Resize(width, height))) + .ToList(); - //update Binary - using var memoryStream = new MemoryStream(); - image.Save(memoryStream, GetDefaultImageEncoder(image.Width, image.Height)); - Binary = memoryStream.ToArray(); + using (var memoryStream = new MemoryStream()) + { + resized[0].Save(memoryStream, GetDefaultImageEncoder(resized[0].Width, resized[0].Height)); + Binary = memoryStream.ToArray(); + } - return [image]; + return resized; }); ForceLoadLazyImage();