Skip to content

Commit 2cae046

Browse files
committed
DW-9: Support for load image with original bpp
1 parent 3600025 commit 2cae046

6 files changed

Lines changed: 155 additions & 3 deletions

File tree

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,71 @@ public void Should_Return_BitsPerPixel()
700700
Assert.Equal(32, bitmap.BitsPerPixel);
701701
}
702702

703+
[TheoryWithAutomaticDisplayName]
704+
[InlineData("ScanDev_BW.tif", 1)]
705+
[InlineData("ScanDev_Gray.tif", 8)]
706+
[InlineData("ScanDev_Color.tif", 24)]
707+
public void DW_9_LoadImage_ShouldReturnOriginalBitsPerPixel(string fileName, int expectedBitsPerPixel)
708+
{
709+
string imagePath = GetRelativeFilePath(fileName);
710+
711+
var bitmap = AnyBitmap.FromFile(imagePath);
712+
713+
Assert.Equal(expectedBitsPerPixel, bitmap.BitsPerPixel);
714+
}
715+
716+
[IgnoreOnUnixFact]
717+
public void DW_9_LoadBlackAndWhiteTiff_ShouldReturnOriginalBitsPerPixel_AndAllowChangingBpp()
718+
{
719+
string imagePath = GetRelativeFilePath("tifimg.tif");
720+
721+
var bitmap = AnyBitmap.FromFile(imagePath);
722+
723+
Assert.Equal(1, bitmap.BitsPerPixel);
724+
Assert.Equal(PixelFormat.Format1bppIndexed, new Bitmap(imagePath).PixelFormat);
725+
726+
var converted = bitmap.ChangeBitsPerPixel(24);
727+
728+
Assert.Equal(24, converted.BitsPerPixel);
729+
Assert.Equal(bitmap.Width, converted.Width);
730+
Assert.Equal(bitmap.Height, converted.Height);
731+
}
732+
733+
[FactWithAutomaticDisplayName]
734+
public void DW_9_LoadImage_NotPreservingOriginalFormat_ShouldReturn32BitsPerPixel()
735+
{
736+
string imagePath = GetRelativeFilePath("ScanDev_BW.tif");
737+
738+
var bitmap = AnyBitmap.FromFile(imagePath, preserveOriginalFormat: false);
739+
740+
Assert.Equal(32, bitmap.BitsPerPixel);
741+
}
742+
743+
[TheoryWithAutomaticDisplayName]
744+
[InlineData(8)]
745+
[InlineData(24)]
746+
[InlineData(32)]
747+
public void DW_9_ChangeBitsPerPixel_ShouldReturnRequestedColorDepth(int targetBitsPerPixel)
748+
{
749+
string imagePath = GetRelativeFilePath("ScanDev_BW.tif");
750+
var bitmap = AnyBitmap.FromFile(imagePath);
751+
752+
var converted = bitmap.ChangeBitsPerPixel(targetBitsPerPixel);
753+
754+
Assert.Equal(targetBitsPerPixel, converted.BitsPerPixel);
755+
Assert.Equal(bitmap.Width, converted.Width);
756+
Assert.Equal(bitmap.Height, converted.Height);
757+
}
758+
759+
[FactWithAutomaticDisplayName]
760+
public void DW_9_ChangeBitsPerPixel_WithUnsupportedDepth_ShouldThrow()
761+
{
762+
string imagePath = GetRelativeFilePath("ScanDev_BW.tif");
763+
var bitmap = AnyBitmap.FromFile(imagePath);
764+
765+
Assert.Throws<NotSupportedException>(() => bitmap.ChangeBitsPerPixel(16));
766+
}
767+
703768
[TheoryWithAutomaticDisplayName()]
704769
[InlineData("mountainclimbers.jpg", "image/jpeg", AnyBitmap.ImageFormat.Jpeg)]
705770
[InlineData("watermark.deployment.png", "image/png", AnyBitmap.ImageFormat.Png)]

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

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -980,13 +980,56 @@ public static AnyBitmap LoadAnyBitmapFromRGBBuffer(byte[] buffer, int width, int
980980

981981
//cache
982982
private int? _bitsPerPixel = null;
983+
984+
/// <summary>
985+
/// The color depth (bits per pixel) of the original source image when it can be
986+
/// determined from the source metadata (e.g. TIFF). SixLabors.ImageSharp has no
987+
/// pixel format below 8bpp, so indexed/bilevel sources would otherwise misreport
988+
/// their depth once decoded into memory (e.g. a 1bpp black &amp; white TIFF that is
989+
/// decoded to a 32bpp Rgba32 image). When set, this is reported by <see cref="BitsPerPixel"/>.
990+
/// </summary>
991+
private int? _originalBitsPerPixel = null;
992+
993+
//cache of the bits per pixel of the in-memory (decoded) image
994+
private int InMemoryBitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel;
995+
983996
/// <summary>
984997
/// Gets colors depth, in number of bits per pixel.
998+
/// <para>When the image is loaded preserving its original format, this reports the
999+
/// bits per pixel of the original source image (for example, 1 for a black &amp; white
1000+
/// image) rather than the bits per pixel of the in-memory decoded representation.</para>
9851001
/// <br/><para><b>Further Documentation:</b><br/>
9861002
/// <a href="https://ironsoftware.com/open-source/csharp/drawing/examples/get-color-depth/">
9871003
/// Code Example</a></para>
9881004
/// </summary>
989-
public int BitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel;
1005+
public int BitsPerPixel => _originalBitsPerPixel ?? InMemoryBitsPerPixel;
1006+
1007+
/// <summary>
1008+
/// Creates a new <see cref="AnyBitmap"/> with the pixel data converted to the requested
1009+
/// color depth, in number of bits per pixel. This is analogous to changing the
1010+
/// <c>PixelFormat</c> of a <see cref="System.Drawing.Bitmap"/>.
1011+
/// </summary>
1012+
/// <param name="bitsPerPixel">The target color depth. Supported values are
1013+
/// <c>8</c> (grayscale), <c>24</c> (RGB) and <c>32</c> (RGBA).</param>
1014+
/// <returns>A new <see cref="AnyBitmap"/> whose <see cref="BitsPerPixel"/> equals
1015+
/// <paramref name="bitsPerPixel"/>.</returns>
1016+
/// <exception cref="NotSupportedException">Thrown when <paramref name="bitsPerPixel"/>
1017+
/// is not one of the supported values.</exception>
1018+
public AnyBitmap ChangeBitsPerPixel(int bitsPerPixel)
1019+
{
1020+
Image source = GetFirstInternalImage();
1021+
Image converted = bitsPerPixel switch
1022+
{
1023+
8 => source.CloneAs<L8>(),
1024+
24 => source.CloneAs<Rgb24>(),
1025+
32 => source.CloneAs<Rgba32>(),
1026+
_ => throw new NotSupportedException(
1027+
$"Changing bits per pixel to {bitsPerPixel} is not supported. " +
1028+
$"Supported values are 8, 24 and 32.")
1029+
};
1030+
1031+
return new AnyBitmap(converted);
1032+
}
9901033

9911034
//cache
9921035
private int? _frameCount = null;
@@ -2610,8 +2653,17 @@ private void LoadImage(Stream stream, bool preserveOriginalFormat)
26102653
private void LoadImage(ReadOnlySpan<byte> span, bool preserveOriginalFormat)
26112654
{
26122655
Binary = span.ToArray();
2613-
if (Format is TiffFormat)
2656+
if (Format is TiffFormat)
26142657
{
2658+
// TIFFs are decoded into a 32bpp Rgba32 image (via LibTiff or ImageSharp), which
2659+
// loses the original color depth. When preserving the original format, capture the
2660+
// source bits per pixel from the TIFF metadata so BitsPerPixel reports it faithfully
2661+
// (e.g. 1 for a black & white image) instead of the decoded 32bpp value.
2662+
if (preserveOriginalFormat)
2663+
{
2664+
_originalBitsPerPixel = GetTiffBitsPerPixelFast();
2665+
}
2666+
26152667
if(GetTiffFrameCountFast() > 1)
26162668
{
26172669
_lazyImage = OpenTiffToImageSharp();
@@ -2825,6 +2877,39 @@ private int GetTiffFrameCountFast()
28252877
}
28262878
}
28272879

2880+
/// <summary>
2881+
/// Reads the original bits per pixel of the first frame of the loaded TIFF directly from
2882+
/// its metadata (BitsPerSample x SamplesPerPixel), without fully decoding the image.
2883+
/// </summary>
2884+
/// <returns>The original bits per pixel, or <c>null</c> if it cannot be determined.</returns>
2885+
private int? GetTiffBitsPerPixelFast()
2886+
{
2887+
try
2888+
{
2889+
using var tiffStream = new MemoryStream(Binary);
2890+
2891+
// Disable error messages for fast check
2892+
Tiff.SetErrorHandler(new DisableErrorHandler());
2893+
2894+
using var tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream());
2895+
if (tiff == null) return null;
2896+
2897+
FieldValue[] bitsPerSampleField = tiff.GetField(TiffTag.BITSPERSAMPLE);
2898+
FieldValue[] samplesPerPixelField = tiff.GetField(TiffTag.SAMPLESPERPIXEL);
2899+
2900+
// BitsPerSample defaults to 1 and SamplesPerPixel defaults to 1 per the TIFF spec.
2901+
int bitsPerSample = bitsPerSampleField != null ? bitsPerSampleField[0].ToInt() : 1;
2902+
int samplesPerPixel = samplesPerPixelField != null ? samplesPerPixelField[0].ToInt() : 1;
2903+
2904+
int bitsPerPixel = bitsPerSample * samplesPerPixel;
2905+
return bitsPerPixel > 0 ? bitsPerPixel : (int?)null;
2906+
}
2907+
catch
2908+
{
2909+
return null; // Fall back to the in-memory pixel depth on any error
2910+
}
2911+
}
2912+
28282913
private Lazy<IReadOnlyList<Image>> OpenTiffToImageSharp()
28292914
{
28302915
return new Lazy<IReadOnlyList<Image>>(() =>
@@ -3114,7 +3199,9 @@ private int GetStride(Image source = null)
31143199
{
31153200
if (source == null)
31163201
{
3117-
return 4 * (((Width * BitsPerPixel) + 31) / 32);
3202+
// Use the in-memory pixel depth (not the reported original BitsPerPixel) so the
3203+
// stride stays consistent with the decoded pixel data exposed by GetFirstPixelData.
3204+
return 4 * (((Width * InMemoryBitsPerPixel) + 31) / 32);
31183205
}
31193206
else
31203207
{

0 commit comments

Comments
 (0)