@@ -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 & 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 & 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