Browse Source

Implement CgBI support

pull/3136/head
Erik White 4 days ago
parent
commit
3e843a06a2
  1. 31
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  2. 73
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  3. 26
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  4. 14
      tests/ImageSharp.Tests/TestImages.cs
  5. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  6. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  7. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  8. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  9. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  10. 3
      tests/Images/Input/Png/cgbi/clocks.png
  11. 3
      tests/Images/Input/Png/cgbi/colors.png
  12. 3
      tests/Images/Input/Png/cgbi/flecks.png
  13. 3
      tests/Images/Input/Png/cgbi/screen.png

31
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -52,12 +52,19 @@ internal sealed class ZlibInflateStream : Stream
/// </summary>
private readonly Func<int> getData;
/// <summary>
/// When true, the inflated payload is treated as a raw DEFLATE stream with no zlib
/// CMF/FLG header (and no Adler-32 trailer). This is required to decode IDATs in
/// Apple's proprietary CgBI PNG variant.
/// </summary>
private readonly bool noHeader;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
: this(innerStream, GetDataNoOp, noHeader: false)
{
}
@ -67,9 +74,23 @@ internal sealed class ZlibInflateStream : Stream
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData)
: this(innerStream, getData, noHeader: false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
/// <param name="noHeader">
/// When <see langword="true"/>, the payload is treated as raw DEFLATE with no zlib header.
/// </param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData, bool noHeader)
{
this.innerStream = innerStream;
this.getData = getData;
this.noHeader = noHeader;
}
/// <inheritdoc/>
@ -210,6 +231,14 @@ internal sealed class ZlibInflateStream : Stream
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Apple CgBI IDATs omit the zlib CMF/FLG header and the Adler-32 trailer,
// wrapping a raw DEFLATE payload directly. Skip the header parsing in that mode.
if (this.noHeader)
{
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
return true;
}
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a

73
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -137,6 +137,13 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// </summary>
private bool hasImageData;
/// <summary>
/// Whether this is an Apple CgBI PNG. CgBI files store IDATs as raw DEFLATE
/// (no zlib header/Adler-32) and pixels as premultiplied BGRA, so they need
/// extra inversion steps to round-trip back to standard PNG semantics.
/// </summary>
private bool isCgbi;
/// <summary>
/// Initializes a new instance of the <see cref="PngDecoderCore"/> class.
/// </summary>
@ -314,7 +321,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
case PngChunkType.End:
goto EOF;
case PngChunkType.ProprietaryApple:
PngThrowHelper.ThrowInvalidChunkType("Proprietary Apple PNG detected! This PNG file is not conform to the specification and cannot be decoded.");
this.isCgbi = true;
break;
}
}
@ -517,6 +524,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore
case PngChunkType.End:
goto EOF;
case PngChunkType.ProprietaryApple:
this.isCgbi = true;
break;
default:
if (this.colorMetadataOnly)
{
@ -766,7 +777,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using ZlibInflateStream inflateStream = new(this.currentStream, getData);
using ZlibInflateStream inflateStream = new(this.currentStream, getData, noHeader: this.isCgbi);
if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData))
{
return;
@ -887,6 +898,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
}
if (this.isCgbi)
{
ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
}
this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer);
this.SwapScanlineBuffers();
currentRow++;
@ -1017,6 +1033,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
}
if (this.isCgbi)
{
ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
}
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
this.ProcessInterlacedDefilteredScanline(
frameControl,
@ -2470,4 +2491,52 @@ internal sealed class PngDecoderCore : ImageDecoderCore
private void SwapScanlineBuffers()
=> (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline);
/// <summary>
/// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline.
/// CgBI PNGs are emitted by <c>pngcrush -iphone</c> with channel order swapped
/// from RGB(A) to BGR(A) and RGB samples premultiplied by alpha. This converts
/// the bytes back to standard PNG semantics in place so the existing scanline
/// processors can consume them unchanged. CgBI is only emitted for 8-bit
/// truecolor (with or without alpha); other color types are left alone.
/// </summary>
/// <remarks>
/// See https://theapplewiki.com/wiki/PNG_CgBI_Format
/// </remarks>
/// <param name="scanline">The defiltered pixel bytes (without the leading filter byte).</param>
/// <param name="colorType">The PNG color type from IHDR.</param>
private static void ApplyCgbiTransform(Span<byte> scanline, PngColorType colorType)
{
if (colorType == PngColorType.RgbWithAlpha)
{
for (int i = 0; i + 3 < scanline.Length; i += 4)
{
byte b = scanline[i];
byte g = scanline[i + 1];
byte r = scanline[i + 2];
byte a = scanline[i + 3];
if (a is not 0 and not 255)
{
// Reverse: c' = c * a / 255 => c = round(c' * 255 / a)
int half = a >> 1;
r = (byte)Math.Min(255, ((r * 255) + half) / a);
g = (byte)Math.Min(255, ((g * 255) + half) / a);
b = (byte)Math.Min(255, ((b * 255) + half) / a);
}
scanline[i] = r;
scanline[i + 1] = g;
scanline[i + 2] = b;
scanline[i + 3] = a;
}
}
else if (colorType == PngColorType.Rgb)
{
for (int i = 0; i + 2 < scanline.Length; i += 3)
{
(scanline[i], scanline[i + 2]) = (scanline[i + 2], scanline[i]);
}
}
}
}

26
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -714,26 +714,18 @@ public partial class PngDecoderTests
Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToPixel<Rgba32>().A < 255);
}
// https://github.com/SixLabors/ImageSharp/issues/410
[Theory]
[WithFile(TestImages.Png.Bad.Issue410_MalformedApplePng, PixelTypes.Rgba32)]
public void Issue410_MalformedApplePng<TPixel>(TestImageProvider<TPixel> provider)
[WithFile(TestImages.Png.Cgbi.Issue410, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Colors, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Clocks, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Flecks, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Screen, PixelTypes.Rgba32)]
public void Decode_AppleCgBI<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Exception ex = Record.Exception(
() =>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
image.DebugSave(provider);
// We don't have another x-plat reference decoder that can be compared for this image.
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Png);
}
});
Assert.NotNull(ex);
Assert.Contains("Proprietary Apple PNG detected!", ex.Message);
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
image.DebugSave(provider);
image.CompareToReferenceOutput(provider, ImageComparer.Exact);
}
[Theory]

14
tests/ImageSharp.Tests/TestImages.cs

@ -177,6 +177,17 @@ public static class TestImages
public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png";
}
public static class Cgbi
{
public const string Colors = "Png/cgbi/colors.png";
public const string Clocks = "Png/cgbi/clocks.png";
public const string Flecks = "Png/cgbi/flecks.png";
public const string Screen = "Png/cgbi/screen.png";
// Issue 410: https://github.com/SixLabors/ImageSharp/issues/410
public const string Issue410 = "Png/issues/Issue_410.png";
}
public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
@ -199,9 +210,6 @@ public static class TestImages
// Issue 1047: https://github.com/SixLabors/ImageSharp/issues/1047
public const string Issue1047_BadEndChunk = "Png/issues/Issue_1047.png";
// Issue 410: https://github.com/SixLabors/ImageSharp/issues/410
public const string Issue410_MalformedApplePng = "Png/issues/Issue_410.png";
// Bad bit depth.
public const string BitDepthZero = "Png/xd0n2c08.png";
public const string BitDepthThree = "Png/xd3n2c08.png";

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:511cb90e72fcb837e4c9a31561a3c914f5201452d4ca63502cb5219cb4dc42be
size 619

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2dc73f1b4435a26125d910f005b5df7a540168a954f44c01a8e46df201adb1b6
size 335851

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8cdcc80a8c662c50d2f72ad8e123d595c6e80394538c05666b8d3531d651e71a
size 11270

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91bbb6c87f128920d4384f3be2e85ecd176e49a7a5166c6c6aa584e60d8131ed
size 204053

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00bb4c7b345389f5d95252c19d70fc2b654c4f0198e6a704b603da92b78e9a0a
size 102982

3
tests/Images/Input/Png/cgbi/clocks.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc462d8c2697060cde9a2e975ffb828d822ee3b0d4d12e3c5f081114176c036b
size 389981

3
tests/Images/Input/Png/cgbi/colors.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f34436f755e3c6d15341f29c992331045ae16ad413144ca798ede5c085c8e6a
size 12853

3
tests/Images/Input/Png/cgbi/flecks.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:314a5a52996953c0c12862262b4ff3fc191d8b6f2b7bb3853cdcea3267a4142d
size 212163

3
tests/Images/Input/Png/cgbi/screen.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e9bfbac37a57b71fa27b38b21314bd49dbe1cb19a2eb9d0f272ec7be3b72a33
size 94834
Loading…
Cancel
Save