Skip to content

Commit b08be16

Browse files
committed
DW-40: carry original BitsPerPixel through GetAllFrames/Clone/RotateFlip/Redact and preserve depth on resize
1 parent d7c1983 commit b08be16

3 files changed

Lines changed: 157 additions & 26 deletions

File tree

Binary file not shown.

‎IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,80 @@ public void DW_9_ChangeBitsPerPixel_DurabilityDependsOnEncoder()
10101010
CleanResultFile("dw9_roundtrip.bmp");
10111011
}
10121012

1013+
[FactWithAutomaticDisplayName]
1014+
public void DW_40_GetAllFrames_ShouldReturnOriginalBitsPerPixelPerFrame()
1015+
{
1016+
var singleFrame = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")).GetAllFrames.ToList();
1017+
Assert.Single(singleFrame);
1018+
Assert.Equal(1, singleFrame[0].BitsPerPixel);
1019+
1020+
// Multi-page TIFF whose pages differ in depth: each frame reports its own source depth.
1021+
var frames = AnyBitmap.FromFile(GetRelativeFilePath("DW-40 MixedDepthMultiPage.tif")).GetAllFrames.ToList();
1022+
Assert.Equal(new[] { 1, 8, 24 }, frames.Select(f => f.BitsPerPixel));
1023+
}
1024+
1025+
[FactWithAutomaticDisplayName]
1026+
public void DW_40_CloneRectangle_ShouldPreserveOriginalBitsPerPixel()
1027+
{
1028+
var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif"));
1029+
1030+
var cropped = bitmap.Clone(new Rectangle(0, 0, 100, 100));
1031+
1032+
Assert.Equal(1, cropped.BitsPerPixel);
1033+
}
1034+
1035+
[FactWithAutomaticDisplayName]
1036+
public void DW_40_RotateFlip_ShouldPreserveOriginalBitsPerPixel()
1037+
{
1038+
var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif"));
1039+
1040+
var rotated = bitmap.RotateFlip(AnyBitmap.RotateMode.Rotate90, AnyBitmap.FlipMode.None);
1041+
1042+
Assert.Equal(1, rotated.BitsPerPixel);
1043+
}
1044+
1045+
[FactWithAutomaticDisplayName]
1046+
public void DW_40_Redact_ShouldPreserveOriginalBitsPerPixel()
1047+
{
1048+
var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif"));
1049+
1050+
var redacted = bitmap.Redact(new Rectangle(0, 0, 10, 10), Color.Black);
1051+
1052+
Assert.Equal(1, redacted.BitsPerPixel);
1053+
}
1054+
1055+
[FactWithAutomaticDisplayName]
1056+
public void DW_40_Resize_ShouldReportHonestDepthForSubEightBppSource()
1057+
{
1058+
var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif"));
1059+
Assert.Equal(1, bitmap.BitsPerPixel);
1060+
1061+
var resized = new AnyBitmap(bitmap, 50, 50);
1062+
1063+
Assert.Equal(32, resized.BitsPerPixel);
1064+
}
1065+
1066+
[FactWithAutomaticDisplayName]
1067+
public void DW_40_Resize_ShouldPreserveDepthForRepresentableFormats()
1068+
{
1069+
var rgb24 = AnyBitmap.FromFile(GetRelativeFilePath("24_bit.png"));
1070+
Assert.Equal(24, rgb24.BitsPerPixel);
1071+
Assert.Equal(24, new AnyBitmap(rgb24, 20, 20).BitsPerPixel);
1072+
1073+
string path64 = "dw40_tmp64.png";
1074+
using (var img64 = new Image<Rgba64>(30, 30)) { img64.SaveAsPng(path64); }
1075+
try
1076+
{
1077+
var rgba64 = AnyBitmap.FromFile(path64);
1078+
Assert.Equal(64, rgba64.BitsPerPixel);
1079+
Assert.Equal(64, new AnyBitmap(rgba64, 15, 15).BitsPerPixel);
1080+
}
1081+
finally
1082+
{
1083+
CleanResultFile(path64);
1084+
}
1085+
}
1086+
10131087
[TheoryWithAutomaticDisplayName()]
10141088
[InlineData("mountainclimbers.jpg", "image/jpeg", AnyBitmap.ImageFormat.Jpeg)]
10151089
[InlineData("watermark.deployment.png", "image/png", AnyBitmap.ImageFormat.Png)]

‎IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs‎

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@ public AnyBitmap Clone()
225225
public AnyBitmap Clone(Rectangle rectangle)
226226
{
227227
var cloned = GetInternalImages().Select(img => img.Clone(x => x.Crop(rectangle)));
228-
return new AnyBitmap(cloned);
228+
var result = new AnyBitmap(cloned);
229+
// Cropping preserves the source color depth, so carry the original depth over (otherwise
230+
// it would fall back to the decoded 32bpp value).
231+
result._originalBitsPerPixel = _originalBitsPerPixel;
232+
return result;
229233
}
230234

231235
/// <summary>
@@ -695,9 +699,15 @@ public AnyBitmap(Stream stream, bool preserveOriginalFormat)
695699
}
696700

697701
/// <summary>
698-
///
702+
/// Creates a new <see cref="AnyBitmap"/> by resizing <paramref name="original"/> to the
703+
/// given dimensions.
704+
/// <para><b>Color depth:</b> resizing resamples (blends) pixels, so <see cref="BitsPerPixel"/>
705+
/// of the result reflects the resized image and may differ from the source. In particular a
706+
/// source whose original depth is below 8bpp (e.g. a 1bpp black &amp; white image) cannot exist
707+
/// below 8bpp in memory, so the resized result reports its actual decoded depth rather than the
708+
/// original low depth. The depth of 8/24/32/64bpp sources is otherwise preserved.</para>
699709
/// </summary>
700-
/// <param name="original">The <see cref="AnyBitmap"/> from which to
710+
/// <param name="original">The <see cref="AnyBitmap"/> from which to
701711
/// create the new <see cref="AnyBitmap"/>.</param>
702712
/// <param name="width">The width of the new AnyBitmap.</param>
703713
/// <param name="height">The height of the new AnyBitmap.</param>
@@ -990,6 +1000,14 @@ public static AnyBitmap LoadAnyBitmapFromRGBBuffer(byte[] buffer, int width, int
9901000
/// </summary>
9911001
private int? _originalBitsPerPixel = null;
9921002

1003+
/// <summary>
1004+
/// The original color depth (bits per pixel) of each frame of a multi-page TIFF, in the same
1005+
/// order as the decoded frames. Populated while decoding the TIFF so that <see cref="GetAllFrames"/>
1006+
/// can report each frame's true source depth (pages of a TIFF may differ in depth). Remains
1007+
/// <c>null</c> for non-TIFF sources.
1008+
/// </summary>
1009+
private IReadOnlyList<int?> _framesOriginalBitsPerPixel = null;
1010+
9931011
//cache of the bits per pixel of the in-memory (decoded) image
9941012
private int InMemoryBitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel;
9951013

@@ -1085,14 +1103,26 @@ public IEnumerable<AnyBitmap> GetAllFrames
10851103
get
10861104
{
10871105
var images = GetInternalImages();
1088-
if (images.Count == 1)
1106+
IEnumerable<AnyBitmap> frames = images.Count == 1
1107+
? ImageFrameCollectionToImages(images[0].Frames).Select(x => (AnyBitmap)x)
1108+
: images.Select(x => (AnyBitmap)x);
1109+
1110+
// Each frame is a freshly-cast AnyBitmap. Carry the captured original source depth
1111+
// onto each frame. Only do this when this object captured an original depth (i.e. a
1112+
// TIFF loaded preserving its original format); otherwise leave frames as-is.
1113+
if (_originalBitsPerPixel == null)
10891114
{
1090-
return ImageFrameCollectionToImages(images[0].Frames).Select(x => (AnyBitmap)x);
1115+
return frames;
10911116
}
1092-
else
1117+
1118+
IReadOnlyList<int?> perFrame = _framesOriginalBitsPerPixel;
1119+
return frames.Select((frame, index) =>
10931120
{
1094-
return images.Select(x => (AnyBitmap)x);
1095-
}
1121+
frame._originalBitsPerPixel = perFrame != null && index < perFrame.Count
1122+
? perFrame[index]
1123+
: _originalBitsPerPixel;
1124+
return frame;
1125+
});
10961126
}
10971127
}
10981128

@@ -1375,7 +1405,11 @@ public static AnyBitmap RotateFlip(
13751405

13761406
image.Mutate(x => x.RotateFlip(rotateModeImgSharp, flipModeImgSharp));
13771407

1378-
return new AnyBitmap(image);
1408+
var result = new AnyBitmap(image);
1409+
// Rotating/flipping preserves the source color depth, so carry the original depth over
1410+
// (otherwise it would fall back to the decoded 32bpp value).
1411+
result._originalBitsPerPixel = bitmap._originalBitsPerPixel;
1412+
return result;
13791413
}
13801414

13811415
/// <summary>
@@ -1411,8 +1445,12 @@ public static AnyBitmap Redact(
14111445
Rectangle rectangle = Rectangle;
14121446
var brush = new SolidBrush(color);
14131447
image.Mutate(ctx => ctx.Fill(brush, rectangle));
1414-
1415-
return new AnyBitmap(image);
1448+
1449+
var result = new AnyBitmap(image);
1450+
// Redacting a region preserves the source color depth, so carry the original depth over
1451+
// (otherwise it would fall back to the decoded 32bpp value).
1452+
result._originalBitsPerPixel = bitmap._originalBitsPerPixel;
1453+
return result;
14161454
}
14171455

14181456
/// <summary>
@@ -2959,25 +2997,37 @@ public override void WarningHandlerExt(Tiff tif, object clientData, string metho
29592997
using var tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream());
29602998
if (tiff == null) return (1, null); // Default to single frame if can't read
29612999

2962-
// Read frame-0 fields before NumberOfDirectories(), which may move the active directory.
2963-
FieldValue[] bitsPerSampleField = tiff.GetField(TiffTag.BITSPERSAMPLE);
2964-
FieldValue[] samplesPerPixelField = tiff.GetField(TiffTag.SAMPLESPERPIXEL);
2965-
2966-
// BitsPerSample defaults to 1 and SamplesPerPixel defaults to 1 per the TIFF spec.
2967-
int bitsPerSample = bitsPerSampleField != null ? bitsPerSampleField[0].ToInt() : 1;
2968-
int samplesPerPixel = samplesPerPixelField != null ? samplesPerPixelField[0].ToInt() : 1;
2969-
int bitsPerPixel = bitsPerSample * samplesPerPixel;
3000+
// Read frame-0 depth before NumberOfDirectories(), which may move the active directory.
3001+
int? bitsPerPixel = ReadDirectoryBitsPerPixel(tiff);
29703002

29713003
int frameCount = tiff.NumberOfDirectories();
29723004

2973-
return (frameCount, bitsPerPixel > 0 ? bitsPerPixel : (int?)null);
3005+
return (frameCount, bitsPerPixel);
29743006
}
29753007
catch
29763008
{
29773009
return (1, null); // Default to single frame / unknown depth on any error
29783010
}
29793011
}
29803012

3013+
/// <summary>
3014+
/// Reads the original bits per pixel (BitsPerSample x SamplesPerPixel) of the TIFF directory
3015+
/// that is currently active on <paramref name="tiff"/>.
3016+
/// </summary>
3017+
/// <returns>The bits per pixel, or <c>null</c> if it cannot be determined.</returns>
3018+
private static int? ReadDirectoryBitsPerPixel(Tiff tiff)
3019+
{
3020+
FieldValue[] bitsPerSampleField = tiff.GetField(TiffTag.BITSPERSAMPLE);
3021+
FieldValue[] samplesPerPixelField = tiff.GetField(TiffTag.SAMPLESPERPIXEL);
3022+
3023+
// BitsPerSample defaults to 1 and SamplesPerPixel defaults to 1 per the TIFF spec.
3024+
int bitsPerSample = bitsPerSampleField != null ? bitsPerSampleField[0].ToInt() : 1;
3025+
int samplesPerPixel = samplesPerPixelField != null ? samplesPerPixelField[0].ToInt() : 1;
3026+
int bitsPerPixel = bitsPerSample * samplesPerPixel;
3027+
3028+
return bitsPerPixel > 0 ? bitsPerPixel : (int?)null;
3029+
}
3030+
29813031
private Lazy<IReadOnlyList<Image>> OpenTiffToImageSharp()
29823032
{
29833033
return new Lazy<IReadOnlyList<Image>>(() =>
@@ -3011,6 +3061,8 @@ private IReadOnlyList<Image> InternalLoadTiff()
30113061
// Disable warning messages
30123062
Tiff.SetErrorHandler(new DisableErrorHandler());
30133063
List<Image> images = new();
3064+
// Original source depth per decoded frame.
3065+
List<int?> framesBitsPerPixel = new();
30143066
// open a TIFF stored in the stream
30153067
using (Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()))
30163068
{
@@ -3026,6 +3078,9 @@ private IReadOnlyList<Image> InternalLoadTiff()
30263078
continue;
30273079
}
30283080

3081+
// Capture this page's original depth before ReadRGBAImage decodes it to 32bpp.
3082+
int? frameBitsPerPixel = ReadDirectoryBitsPerPixel(tiff);
3083+
30293084
var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution);
30303085

30313086
// Read the image into the memory buffer
@@ -3036,18 +3091,20 @@ private IReadOnlyList<Image> InternalLoadTiff()
30363091
}
30373092

30383093
var bits = PrepareByteArray(raster, width, height, 32);
3039-
3094+
30403095
var image = Image.LoadPixelData<Rgba32>(bits, width, height);
30413096

30423097
image.Metadata.HorizontalResolution = horizontalResolution;
30433098
image.Metadata.VerticalResolution = verticalResolution;
30443099
images.Add(image);
3100+
framesBitsPerPixel.Add(frameBitsPerPixel);
30453101

30463102
//Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff
30473103
//Note2: 'yield return' make it super slow
30483104
}
30493105

30503106
}
3107+
_framesOriginalBitsPerPixel = framesBitsPerPixel;
30513108
return images;
30523109
}
30533110

@@ -3386,14 +3443,14 @@ private void SetPixelColor(int x, int y, Color color)
33863443

33873444
private void LoadAndResizeImage(AnyBitmap original, int width, int height)
33883445
{
3389-
//this prevent case when original is changed before Lazy is loaded
3390-
Binary = original.Binary;
3446+
// Capture the source's decoded image up front so we are independent of the original's lifetime.
3447+
Image sourceImage = original.GetFirstInternalImage();
33913448

33923449
_lazyImage = new Lazy<IReadOnlyList<Image>>(() =>
33933450
{
3394-
3395-
var image = Image.Load<Rgba32>(Binary);
3396-
image.Mutate(img => img.Resize(width, height));
3451+
// Resize a clone of the already-decoded image so its pixel type (and therefore its
3452+
// color depth) is preserved.
3453+
var image = sourceImage.Clone(img => img.Resize(width, height));
33973454

33983455
//update Binary
33993456
using var memoryStream = new MemoryStream();

0 commit comments

Comments
 (0)