Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,80 @@ public void DW_9_ChangeBitsPerPixel_DurabilityDependsOnEncoder()
CleanResultFile("dw9_roundtrip.bmp");
}

[FactWithAutomaticDisplayName]
public void DW_40_GetAllFrames_ShouldReturnOriginalBitsPerPixelPerFrame()
{
var singleFrame = AnyBitmap.FromFile(GetRelativeFilePath("ScanDev_BW.tif")).GetAllFrames.ToList();
Assert.Single(singleFrame);
Assert.Equal(1, singleFrame[0].BitsPerPixel);

// Multi-page TIFF whose pages differ in depth: each frame reports its own source depth.
var frames = AnyBitmap.FromFile(GetRelativeFilePath("DW-40 MixedDepthMultiPage.tif")).GetAllFrames.ToList();
Assert.Equal(new[] { 1, 8, 24 }, frames.Select(f => 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<Rgba64>(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)]
Expand Down
109 changes: 83 additions & 26 deletions IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,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;
}

/// <summary>
Expand Down Expand Up @@ -695,9 +699,15 @@ public AnyBitmap(Stream stream, bool preserveOriginalFormat)
}

/// <summary>
///
/// Creates a new <see cref="AnyBitmap"/> by resizing <paramref name="original"/> to the
/// given dimensions.
/// <para><b>Color depth:</b> resizing resamples (blends) pixels, so <see cref="BitsPerPixel"/>
/// 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 &amp; 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.</para>
/// </summary>
/// <param name="original">The <see cref="AnyBitmap"/> from which to
/// <param name="original">The <see cref="AnyBitmap"/> from which to
/// create the new <see cref="AnyBitmap"/>.</param>
/// <param name="width">The width of the new AnyBitmap.</param>
/// <param name="height">The height of the new AnyBitmap.</param>
Expand Down Expand Up @@ -990,6 +1000,14 @@ public static AnyBitmap LoadAnyBitmapFromRGBBuffer(byte[] buffer, int width, int
/// </summary>
private int? _originalBitsPerPixel = null;

/// <summary>
/// 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 <see cref="GetAllFrames"/>
/// can report each frame's true source depth (pages of a TIFF may differ in depth). Remains
/// <c>null</c> for non-TIFF sources.
/// </summary>
private IReadOnlyList<int?> _framesOriginalBitsPerPixel = null;

//cache of the bits per pixel of the in-memory (decoded) image
private int InMemoryBitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel;

Expand Down Expand Up @@ -1085,14 +1103,26 @@ public IEnumerable<AnyBitmap> GetAllFrames
get
{
var images = GetInternalImages();
if (images.Count == 1)
IEnumerable<AnyBitmap> 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<int?> 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;
});
}
}

Expand Down Expand Up @@ -1375,7 +1405,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;
}

/// <summary>
Expand Down Expand Up @@ -1411,8 +1445,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;
}

/// <summary>
Expand Down Expand Up @@ -2959,25 +2997,37 @@ 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
{
return (1, null); // Default to single frame / unknown depth on any error
}
}

/// <summary>
/// Reads the original bits per pixel (BitsPerSample x SamplesPerPixel) of the TIFF directory
/// that is currently active on <paramref name="tiff"/>.
/// </summary>
/// <returns>The bits per pixel, or <c>null</c> if it cannot be determined.</returns>
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<IReadOnlyList<Image>> OpenTiffToImageSharp()
{
return new Lazy<IReadOnlyList<Image>>(() =>
Expand Down Expand Up @@ -3011,6 +3061,8 @@ private IReadOnlyList<Image> InternalLoadTiff()
// Disable warning messages
Tiff.SetErrorHandler(new DisableErrorHandler());
List<Image> images = new();
// Original source depth per decoded frame.
List<int?> framesBitsPerPixel = new();
// open a TIFF stored in the stream
using (Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()))
{
Expand All @@ -3026,6 +3078,9 @@ private IReadOnlyList<Image> InternalLoadTiff()
continue;
}

// Capture this page's original depth before ReadRGBAImage decodes it to 32bpp.
int? frameBitsPerPixel = ReadDirectoryBitsPerPixel(tiff);

var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution);

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

var bits = PrepareByteArray(raster, width, height, 32);

var image = Image.LoadPixelData<Rgba32>(bits, width, height);

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
}

}
_framesOriginalBitsPerPixel = framesBitsPerPixel;
return images;
}

Expand Down Expand Up @@ -3386,14 +3443,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<IReadOnlyList<Image>>(() =>
{

var image = Image.Load<Rgba32>(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();
Expand Down
Loading