-
-
Notifications
You must be signed in to change notification settings - Fork 20
DW-9: Load image using original BPP and support changing color depth #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -980,13 +980,71 @@ public static AnyBitmap LoadAnyBitmapFromRGBBuffer(byte[] buffer, int width, int | |
|
|
||
| //cache | ||
| private int? _bitsPerPixel = null; | ||
|
|
||
| /// <summary> | ||
| /// The color depth (bits per pixel) of the original source image when it can be | ||
| /// determined from the source metadata (e.g. TIFF). SixLabors.ImageSharp has no | ||
| /// pixel format below 8bpp, so indexed/bilevel sources would otherwise misreport | ||
| /// their depth once decoded into memory (e.g. a 1bpp black & white TIFF that is | ||
| /// decoded to a 32bpp Rgba32 image). When set, this is reported by <see cref="BitsPerPixel"/>. | ||
| /// </summary> | ||
| private int? _originalBitsPerPixel = null; | ||
|
|
||
| //cache of the bits per pixel of the in-memory (decoded) image | ||
| private int InMemoryBitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel; | ||
|
|
||
| /// <summary> | ||
| /// Gets colors depth, in number of bits per pixel. | ||
| /// <para>When the image is loaded preserving its original format, this reports the | ||
| /// bits per pixel of the original source image (for example, 1 for a black & white | ||
| /// image) rather than the bits per pixel of the in-memory decoded representation.</para> | ||
| /// <para><b>Important:</b> this value is intentionally decoupled from <see cref="Stride"/> | ||
| /// and <see cref="Scan0"/>. SixLabors.ImageSharp has no pixel format below 8bpp, so a | ||
| /// source with a lower color depth (e.g. a 1bpp black & white TIFF) is always decoded | ||
| /// into a 32bpp buffer in memory. <see cref="BitsPerPixel"/> reports the <i>original</i> | ||
| /// depth, whereas <see cref="Stride"/> and <see cref="Scan0"/> describe the <i>decoded</i> | ||
| /// 32bpp BGRA buffer. Do not size a <see cref="Scan0"/> buffer using | ||
| /// <c>Width * Height * BitsPerPixel / 8</c>; use <see cref="Stride"/> instead.</para> | ||
| /// <br/><para><b>Further Documentation:</b><br/> | ||
| /// <a href="https://ironsoftware.com/open-source/csharp/drawing/examples/get-color-depth/"> | ||
| /// Code Example</a></para> | ||
| /// </summary> | ||
| public int BitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel; | ||
| public int BitsPerPixel => _originalBitsPerPixel ?? InMemoryBitsPerPixel; | ||
|
|
||
| /// <summary> | ||
| /// Creates a new <see cref="AnyBitmap"/> with the pixel data converted to the requested | ||
| /// color depth, in number of bits per pixel. This is analogous to changing the | ||
| /// <c>PixelFormat</c> of a <see cref="System.Drawing.Bitmap"/>. | ||
| /// <para><b>Single frame:</b> only the first frame is converted. For a multi-page TIFF or | ||
| /// animated GIF the additional frames are not carried over to the returned image.</para> | ||
| /// <para><b>Durability:</b> the conversion changes the in-memory pixel representation. | ||
| /// Whether the new depth survives a save depends on the target encoder: formats that honor | ||
| /// the pixel type (e.g. PNG) preserve it, whereas saving without an explicit format or | ||
| /// calling <see cref="GetBytes()"/> re-encodes via the default 32bpp BMP encoder and the | ||
| /// reduced depth is lost.</para> | ||
| /// </summary> | ||
| /// <param name="bitsPerPixel">The target color depth. Supported values are | ||
| /// <c>8</c> (grayscale), <c>24</c> (RGB) and <c>32</c> (RGBA).</param> | ||
| /// <returns>A new <see cref="AnyBitmap"/> whose <see cref="BitsPerPixel"/> equals | ||
| /// <paramref name="bitsPerPixel"/>.</returns> | ||
| /// <exception cref="NotSupportedException">Thrown when <paramref name="bitsPerPixel"/> | ||
| /// is not one of the supported values.</exception> | ||
| public AnyBitmap ChangeBitsPerPixel(int bitsPerPixel) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: The converted depth only lives in the in-memory
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified empirically (PNG is durable → 8; default BMP/GetBytes → 32). Documented the durability behavior precisely in the XML doc and locked it with a round-trip test. |
||
| { | ||
| // Note: GetFirstInternalImage() only exposes frame 0, so this converts the first frame only. | ||
| Image source = GetFirstInternalImage(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documented single-frame behavior in the XML doc + inline comment. |
||
| Image converted = bitsPerPixel switch | ||
| { | ||
| 8 => source.CloneAs<L8>(), | ||
| 24 => source.CloneAs<Rgb24>(), | ||
| 32 => source.CloneAs<Rgba32>(), | ||
| _ => throw new NotSupportedException( | ||
| $"Changing bits per pixel to {bitsPerPixel} is not supported. " + | ||
| $"Supported values are 8, 24 and 32.") | ||
| }; | ||
|
|
||
| return new AnyBitmap(converted); | ||
| } | ||
|
|
||
| //cache | ||
| private int? _frameCount = null; | ||
|
|
@@ -1300,8 +1358,11 @@ public static AnyBitmap Redact( | |
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the stride width (also called scan width) of the | ||
| /// Gets the stride width (also called scan width) of the | ||
| /// <see cref="AnyBitmap"/> object. | ||
| /// <para>This describes the in-memory decoded 32bpp BGRA buffer (see <see cref="Scan0"/>) | ||
| /// and is therefore independent of <see cref="BitsPerPixel"/>, which may report the lower | ||
| /// original color depth of the source image.</para> | ||
| /// </summary> | ||
| public int Stride | ||
| { | ||
|
|
@@ -1312,11 +1373,15 @@ public int Stride | |
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the address of the first pixel data in the | ||
| /// <see cref="AnyBitmap"/>. This can also be thought of as the first | ||
| /// Gets the address of the first pixel data in the | ||
| /// <see cref="AnyBitmap"/>. This can also be thought of as the first | ||
| /// scan line in the <see cref="AnyBitmap"/>. | ||
| /// <para>The pixel data is always the in-memory 32bpp BGRA representation, regardless of | ||
| /// the value reported by <see cref="BitsPerPixel"/> (which may be the lower original | ||
| /// color depth of the source image). Pair this with <see cref="Stride"/>, not | ||
| /// <see cref="BitsPerPixel"/>, when computing buffer sizes.</para> | ||
| /// </summary> | ||
| /// <returns>The address of the first 32bpp BGRA pixel data in the | ||
| /// <returns>The address of the first 32bpp BGRA pixel data in the | ||
| /// <see cref="AnyBitmap"/>.</returns> | ||
| public IntPtr Scan0 | ||
| { | ||
|
|
@@ -2610,9 +2675,21 @@ private void LoadImage(Stream stream, bool preserveOriginalFormat) | |
| private void LoadImage(ReadOnlySpan<byte> span, bool preserveOriginalFormat) | ||
| { | ||
| Binary = span.ToArray(); | ||
| if (Format is TiffFormat) | ||
| if (Format is TiffFormat) | ||
| { | ||
| if(GetTiffFrameCountFast() > 1) | ||
| // Read frame count and original bits per pixel in a single metadata pass. | ||
| var (frameCount, originalBitsPerPixel) = ReadTiffMetadataFast(); | ||
|
|
||
| // TIFFs are decoded into a 32bpp Rgba32 image (via LibTiff or ImageSharp), which | ||
| // loses the original color depth. When preserving the original format, capture the | ||
| // source bits per pixel from the TIFF metadata so BitsPerPixel reports it faithfully | ||
| // (e.g. 1 for a black & white image) instead of the decoded 32bpp value. | ||
| if (preserveOriginalFormat) | ||
| { | ||
| _originalBitsPerPixel = originalBitsPerPixel; | ||
| } | ||
|
|
||
| if (frameCount > 1) | ||
| { | ||
| _lazyImage = OpenTiffToImageSharp(); | ||
| } | ||
|
|
@@ -2621,7 +2698,7 @@ private void LoadImage(ReadOnlySpan<byte> span, bool preserveOriginalFormat) | |
| // ImageSharp can load some single frame tiff, if failed we try again with LibTiff | ||
| _lazyImage = OpenImageToImageSharp(preserveOriginalFormat, tryWithLibTiff : true); | ||
| } | ||
|
|
||
| } | ||
| else | ||
| { | ||
|
|
@@ -2805,7 +2882,14 @@ public override void WarningHandlerExt(Tiff tif, object clientData, string metho | |
| } | ||
| } | ||
|
|
||
| private int GetTiffFrameCountFast() | ||
| /// <summary> | ||
| /// Reads lightweight TIFF metadata, i.e. the number of frames (directories) and the original | ||
| /// bits per pixel of the first frame (BitsPerSample x SamplesPerPixel), in a single pass, | ||
| /// without fully decoding the image. | ||
| /// </summary> | ||
| /// <returns>A tuple of the frame count (defaults to 1 if it cannot be read) and the original | ||
| /// bits per pixel of the first frame (<c>null</c> if it cannot be determined).</returns> | ||
| private (int FrameCount, int? BitsPerPixel) ReadTiffMetadataFast() | ||
| { | ||
| try | ||
| { | ||
|
|
@@ -2815,13 +2899,24 @@ private int GetTiffFrameCountFast() | |
| Tiff.SetErrorHandler(new DisableErrorHandler()); | ||
|
|
||
| using var tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()); | ||
| if (tiff == null) return 1; // Default to single frame if can't read | ||
| 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; | ||
|
|
||
| int frameCount = tiff.NumberOfDirectories(); | ||
|
|
||
| return tiff.NumberOfDirectories(); | ||
| return (frameCount, bitsPerPixel > 0 ? bitsPerPixel : (int?)null); | ||
| } | ||
| catch | ||
| { | ||
| return 1; // Default to single frame on any error | ||
| return (1, null); // Default to single frame / unknown depth on any error | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -3114,7 +3209,9 @@ private int GetStride(Image source = null) | |
| { | ||
| if (source == null) | ||
| { | ||
| return 4 * (((Width * BitsPerPixel) + 31) / 32); | ||
| // Use the in-memory pixel depth (not the reported original BitsPerPixel) so the | ||
| // stride stays consistent with the decoded pixel data exposed by GetFirstPixelData. | ||
| return 4 * (((Width * InMemoryBitsPerPixel) + 31) / 32); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: This decoupling is correct internally (Stride stays 32-based, consistent with the 32bpp BGRA buffer from
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documented the divergence explicitly on |
||
| } | ||
| else | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Happy-path only.
ChangeBitsPerPixel(32)here asserts a clone is 32bpp (it already was — proves nothing), and no test round-trips a converted bitmap throughSaveAs/GetBytesand reloads — which is exactly why the non-durable-depth issue above is invisible. Suggest adding a round-trip test (it will currently fail, documenting the gap) and a Stride/Scan0-vs-BitsPerPixel consistency assertion so the intentional decoupling is locked against future re-coupling.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
DW_9_BitsPerPixel_IsIntentionallyDecoupledFromStrideAndScan0(locks the decoupling) andDW_9_ChangeBitsPerPixel_DurabilityDependsOnEncoder(PNG→8, BMP/GetBytes→32).