DW-40: Carry original BitsPerPixel through derived AnyBitmap operations and preserve depth on resize#154
Open
Sawraz-IS wants to merge 1 commit into
Open
Conversation
ikkyuland
approved these changes
Jul 9, 2026
ikkyuland
reviewed
Jul 9, 2026
Comment on lines
+3446
to
+3457
| // Resize a clone of the already-decoded image so its pixel type (and therefore its color | ||
| // depth) is preserved. | ||
| var image = original.GetFirstInternalImage().Clone(img => img.Resize(width, height)); | ||
|
|
||
| _lazyImage = new Lazy<IReadOnlyList<Image>>(() => | ||
| //update Binary | ||
| using (var memoryStream = new MemoryStream()) | ||
| { | ||
|
|
||
| var image = Image.Load<Rgba32>(Binary); | ||
| image.Mutate(img => img.Resize(width, height)); | ||
|
|
||
| //update Binary | ||
| using var memoryStream = new MemoryStream(); | ||
| image.Save(memoryStream, GetDefaultImageEncoder(image.Width, image.Height)); | ||
| Binary = memoryStream.ToArray(); | ||
| } | ||
|
|
||
| return [image]; | ||
| }); | ||
| _lazyImage = new Lazy<IReadOnlyList<Image>>(() => [image]); |
Member
There was a problem hiding this comment.
actually we should move this code to be inside Lazy
//but this method call ForceLoadLazyImage(); anyway so it's not a deal breaker
Collaborator
Author
There was a problem hiding this comment.
Good call, thanks. I have moved the clone, resize and encode work back inside the Lazy so the lazy-loading pattern is preserved. To keep it safe against the original's lifetime (what the old Binary = original.Binary line guarded), I capture the source's decoded image reference up front and then do the work inside the Lazy. Pushed. All AnyBitmapFunctionality tests still pass (123/123 net8.0, 121/121 net48).
…lip/Redact and preserve depth on resize
abbf129 to
b08be16
Compare
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.
Description
Follow-up to DW-9. DW-9 made
AnyBitmap.BitsPerPixelreport a TIFF's true source color depth (for example 1 for black and white) instead of the decoded 32bpp value, but only for the object returned directly byAnyBitmap.FromFile/FromStream. Several public APIs build a newAnyBitmapfrom an already in-memory image and did not carry that captured depth over, so they silently fell back to reporting 32.This PR carries the original source depth through those derived operations, and fixes the resize path so it no longer flattens every image to 32bpp.
nuspec release notes (not yet pushed). The
NuGet/IronSoftware.Drawing.nuspecrelease notes update is intentionally not part of this branch yet. To be added when the change set is finalized:Fixes DW-40
Root cause. DW-9 stores the captured depth in a private
_originalBitsPerPixelfield that is only populated insideLoadImage.BitsPerPixelreturns_originalBitsPerPixel ?? InMemoryBitsPerPixel. Each derived API constructs its result through an internal constructor (or the ImageSharp cast operator) that never sets_originalBitsPerPixel, so the getter fell back to the in memory decoded depth (32). The resize path additionally hard codedImage.Load<Rgba32>, which forced 32bpp for all inputs.Changes in
AnyBitmap.cs:Clone(Rectangle),RotateFlip(...),Redact(...): these operations preserve the source color depth, so the result now copies_originalBitsPerPixelfrom the source. Propagation is done explicitly in each API (not in the sharedAnyBitmap(Image)constructor, because that same constructor is the ImageSharp cast operator, where there is no original source depth to assume).GetAllFrames: now reports each frame's own source depth. A multi page TIFF can legally mix depths per page (for example page 1 bilevel, page 2 color), so a single frame-0 value would be wrong for later pages. Per frame depth is captured duringInternalLoadTiffinside the same loop that decodes frames and skips thumbnails, guaranteeing index alignment with the decoded frames. Frames only receive an original depth when the object itself captured one (a TIFF loaded preserving its original format); otherwise they report the in memory depth as before.AnyBitmap(AnyBitmap, int, int)(LoadAndResizeImage): now resizes a clone of the already decoded image, preserving its pixel type, instead of re-decoding asRgba32. This fixes two problems: 24bpp is no longer upsampled to 32bpp, and 64bpp (Rgba64) is no longer flattened to 32bpp with loss of color precision (16 bits per channel down to 8). Resizing resamples (blends) pixels, so a source whose original depth is below 8bpp (for example 1bpp, which has no in memory representation below 8bpp) is honestly reported at its decoded depth rather than claiming to still be 1bpp. The original low depth is intentionally not carried over for resize.ReadDirectoryBitsPerPixel(Tiff), shared byReadTiffMetadataFast(frame 0) and the new per frame capture inInternalLoadTiff.Documentation: updated the resize constructor XML doc to state that resizing resamples and may change
BitsPerPixel, consistent with the existingBitsPerPixel/Stride/Scan0decoupling notes added in DW-9.Type of change
How Has This Been Tested?
Added six unit tests in
AnyBitmapFunctionality.cs, run on net48 and net8.0:DW_40_GetAllFrames_ShouldReturnOriginalBitsPerPixelPerFrame: a single frame 1bpp TIFF reports 1, and a mixed depth multi page TIFF reports[1, 8, 24]per frame.DW_40_CloneRectangle_ShouldPreserveOriginalBitsPerPixel: cropping a 1bpp TIFF reports 1.DW_40_RotateFlip_ShouldPreserveOriginalBitsPerPixel: rotating a 1bpp TIFF reports 1.DW_40_Redact_ShouldPreserveOriginalBitsPerPixel: redacting a 1bpp TIFF reports 1.DW_40_Resize_ShouldReportHonestDepthForSubEightBppSource: resizing a 1bpp TIFF honestly reports 32.DW_40_Resize_ShouldPreserveDepthForRepresentableFormats: resizing a 24bpp image reports 24, and resizing a 64bpp image reports 64.New test asset
Data/DW-40 MixedDepthMultiPage.tif: a synthesized 3 page TIFF (page 0 = 1bpp, page 1 = 8bpp grayscale, page 2 = 24bpp RGB) so the per frameGetAllFramesbehavior is verified against genuinely mixed depths.Results: full test project passes locally, 414 of 414 on net8.0 and 400 of 400 on net48, no regressions.
Checklist:
Additional Context
Behavior change summary for a source loaded preserving its original format:
GetAllFrames(per page)Clone(Rectangle)RotateFlip(...)Redact(...)Design decision (resize). Resize was intentionally kept honest for sub 8bpp sources rather than propagating the original depth, because resampling blends black and white into gray, so the resized image genuinely is no longer bilevel. For 8/24/32/64bpp the depth is preserved by resizing at the native pixel type.
Durability note. As with
BitsPerPixelon the loaded object, a carried original depth is an in memory label. Saving through the default encoder (BMP at 32bpp) or callingGetBytes()re-encodes and the reduced depth is not preserved in the saved bytes. This is unchanged behavior and is already documented onBitsPerPixel.