From b94a865d1c4933d1cab2c20167305d5800a23257 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 3 Dec 2016 15:38:01 +1100 Subject: [PATCH] Initial attempt to decode png. Touch #9 I can pull the pixels from the stream but something is wrong with my offset calculations. Fingers crossde it's something easy to spot. --- src/ImageSharp/Formats/Png/InterlaceMode.cs | 23 +++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 170 ++++++++++++++++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngHeader.cs | 2 +- tests/ImageSharp.Tests/FileTestBase.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 3 +- .../Formats/Png/splash-interlaced.png | 3 + 7 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/InterlaceMode.cs create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Png/splash-interlaced.png diff --git a/src/ImageSharp/Formats/Png/InterlaceMode.cs b/src/ImageSharp/Formats/Png/InterlaceMode.cs new file mode 100644 index 0000000000..663c9611ca --- /dev/null +++ b/src/ImageSharp/Formats/Png/InterlaceMode.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Provides enumeration of available PNG interlace modes. + /// + public enum InterlaceMode : byte + { + /// + /// Non interlaced + /// + None, + + /// + /// Adam 7 interlacing. + /// + Adam7 + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 38042426c8..fd8972fbbc 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -24,6 +24,26 @@ namespace ImageSharp.Formats /// private static readonly Dictionary ColorTypes = new Dictionary(); + /// + /// The amount to increment when processing each column per scanline for each interlaced pass + /// + private static readonly int[] Adam7ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 }; + + /// + /// The index to start at when processing each column per scanline for each interlaced pass + /// + private static readonly int[] Adam7FirstColumn = { 0, 4, 0, 2, 0, 1, 0 }; + + /// + /// The index to start at when processing each row per scanline for each interlaced pass + /// + private static readonly int[] Adam7FirstRow = { 0, 0, 4, 0, 2, 0, 1 }; + + /// + /// The amount to increment when processing each row per scanline for each interlaced pass + /// + private static readonly int[] Adam7RowIncrement = { 8, 8, 8, 4, 4, 2, 2 }; + /// /// Reusable buffer for reading chunk types. /// @@ -285,10 +305,13 @@ namespace ImageSharp.Formats /// /// Calculates the scanline length. /// - /// The representing the length. - private int CalculateScanlineLength() + /// The width of the row. + /// + /// The representing the length. + /// + private int CalculateScanlineLength(int width) { - int scanlineLength = this.header.Width * this.header.BitDepth * this.bytesPerPixel; + int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; int amount = scanlineLength % 8; if (amount != 0) @@ -311,7 +334,7 @@ namespace ImageSharp.Formats where TPacked : struct { this.bytesPerPixel = this.CalculateBytesPerPixel(); - this.bytesPerScanline = this.CalculateScanlineLength() + 1; + this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; if (this.header.BitDepth >= 8) { @@ -321,7 +344,14 @@ namespace ImageSharp.Formats dataStream.Position = 0; using (ZlibInflateStream compressedStream = new ZlibInflateStream(dataStream)) { - this.DecodePixelData(compressedStream, pixels); + if (this.header.InterlaceMethod == InterlaceMode.Adam7) + { + this.DecodeInterlacedPixelData(compressedStream, pixels); + } + else + { + this.DecodePixelData(compressedStream, pixels); + } } } @@ -398,6 +428,96 @@ namespace ImageSharp.Formats } } + /// + /// Decodes the raw interlaced pixel data row by row + /// + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The compressed pixel data stream. + /// The image pixel accessor. + private void DecodeInterlacedPixelData(Stream compressedStream, PixelAccessor pixels) + where TColor : struct, IPackedPixel + where TPacked : struct + { + for (int pass = 0; pass < 7; pass++) + { + int y = Adam7FirstRow[pass]; + int numColumns = this.ComputeColumnsAdam7(pass); + + if (numColumns == 0) + { + // This pass contains no data; skip to next pass + continue; + } + + int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; + + while (y < this.header.Height) + { + byte[] previousScanline = ArrayPool.Shared.Rent(this.bytesPerScanline); + byte[] scanline = ArrayPool.Shared.Rent(this.bytesPerScanline); + + // Zero out the previousScanline, because the bytes that are rented from the arraypool may not be zero. + Array.Clear(previousScanline, 0, this.bytesPerScanline); + + try + { + compressedStream.Read(scanline, 0, bytesPerInterlaceScanline); + + FilterType filterType = (FilterType)scanline[0]; + + switch (filterType) + { + case FilterType.None: + + NoneFilter.Decode(scanline); + + break; + + case FilterType.Sub: + + SubFilter.Decode(scanline, bytesPerInterlaceScanline, this.bytesPerPixel); + + break; + + case FilterType.Up: + + UpFilter.Decode(scanline, previousScanline, bytesPerInterlaceScanline); + + break; + + case FilterType.Average: + + AverageFilter.Decode(scanline, previousScanline, bytesPerInterlaceScanline, this.bytesPerPixel); + + break; + + case FilterType.Paeth: + + PaethFilter.Decode(scanline, previousScanline, bytesPerInterlaceScanline, this.bytesPerPixel); + + break; + + default: + throw new ImageFormatException("Unknown filter type."); + } + + this.ProcessDefilteredScanline(scanline, y, pixels, Adam7FirstColumn[pass], Adam7ColumnIncrement[pass]); + + Swap(ref scanline, ref previousScanline); + } + finally + { + ArrayPool.Shared.Return(previousScanline); + ArrayPool.Shared.Return(scanline); + } + + y += Adam7RowIncrement[pass]; + } + } + } + /// /// Processes the de-filtered scanline filling the image pixel data /// @@ -406,17 +526,20 @@ namespace ImageSharp.Formats /// The de-filtered scanline /// The current image row. /// The image pixels - private void ProcessDefilteredScanline(byte[] defilteredScanline, int row, PixelAccessor pixels) + /// The column start index. Always 0 for none interlaced images. + /// The column increment. Always 1 for none interlaced images. + private void ProcessDefilteredScanline(byte[] defilteredScanline, int row, PixelAccessor pixels, int startIndex = 0, int increment = 1) where TColor : struct, IPackedPixel where TPacked : struct { TColor color = default(TColor); + switch (this.PngColorType) { case PngColorType.Grayscale: int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1); byte[] newScanline1 = ToArrayByBitsLength(defilteredScanline, this.bytesPerScanline, this.header.BitDepth); - for (int x = 0; x < this.header.Width; x++) + for (int x = startIndex; x < this.header.Width; x += increment) { byte intensity = (byte)(newScanline1[x] * factor); color.PackFromBytes(intensity, intensity, intensity, 255); @@ -427,7 +550,7 @@ namespace ImageSharp.Formats case PngColorType.GrayscaleWithAlpha: - for (int x = 0; x < this.header.Width; x++) + for (int x = startIndex; x < this.header.Width; x += increment) { int offset = 1 + (x * this.bytesPerPixel); @@ -448,7 +571,7 @@ namespace ImageSharp.Formats { // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha // channel and we should try to read it. - for (int x = 0; x < this.header.Width; x++) + for (int x = startIndex; x < this.header.Width; x += increment) { int index = newScanline[x]; int pixelOffset = index * 3; @@ -472,7 +595,7 @@ namespace ImageSharp.Formats } else { - for (int x = 0; x < this.header.Width; x++) + for (int x = startIndex; x < this.header.Width; x += increment) { int index = newScanline[x]; int pixelOffset = index * 3; @@ -490,7 +613,7 @@ namespace ImageSharp.Formats case PngColorType.Rgb: - for (int x = 0; x < this.header.Width; x++) + for (int x = startIndex; x < this.header.Width; x += increment) { int offset = 1 + (x * this.bytesPerPixel); @@ -506,7 +629,7 @@ namespace ImageSharp.Formats case PngColorType.RgbWithAlpha: - for (int x = 0; x < this.header.Width; x++) + for (int x = startIndex; x < this.header.Width; x += increment) { int offset = 1 + (x * this.bytesPerPixel); @@ -570,7 +693,7 @@ namespace ImageSharp.Formats this.header.ColorType = data[9]; this.header.CompressionMethod = data[10]; this.header.FilterMethod = data[11]; - this.header.InterlaceMethod = data[12]; + this.header.InterlaceMethod = (InterlaceMode)data[12]; } /// @@ -596,10 +719,9 @@ namespace ImageSharp.Formats throw new NotSupportedException("The png specification only defines 0 as filter method."); } - if (this.header.InterlaceMethod != 0) + if (this.header.InterlaceMethod != InterlaceMode.None && this.header.InterlaceMethod != InterlaceMode.Adam7) { - // TODO: Support interlacing - throw new NotSupportedException("Interlacing is not supported."); + throw new NotSupportedException("The png specification only defines 'None' and 'Adam7' as interlaced methods."); } this.PngColorType = (PngColorType)this.header.ColorType; @@ -715,5 +837,21 @@ namespace ImageSharp.Formats chunk.Length = BitConverter.ToInt32(this.chunkLengthBuffer, 0); } + + private int ComputeColumnsAdam7(int pass) + { + int width = this.header.Width; + switch (pass) + { + case 0: return (width + 7) / 8; + case 1: return (width + 3) / 8; + case 2: return (width + 3) / 4; + case 3: return (width + 1) / 4; + case 4: return (width + 1) / 2; + case 5: return width / 2; + case 6: return width; + default: throw new ArgumentException($"Not a valid pass index: {pass}"); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 010bd635d4..434089e08b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -473,7 +473,7 @@ namespace ImageSharp.Formats this.chunkDataBuffer[9] = header.ColorType; this.chunkDataBuffer[10] = header.CompressionMethod; this.chunkDataBuffer[11] = header.FilterMethod; - this.chunkDataBuffer[12] = header.InterlaceMethod; + this.chunkDataBuffer[12] = (byte)header.InterlaceMethod; this.WriteChunk(stream, PngChunkTypes.Header, this.chunkDataBuffer, 0, 13); } diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/PngHeader.cs index 3121969b6f..ffad043f0d 100644 --- a/src/ImageSharp/Formats/Png/PngHeader.cs +++ b/src/ImageSharp/Formats/Png/PngHeader.cs @@ -57,6 +57,6 @@ namespace ImageSharp.Formats /// Indicates the transmission order of the image data. /// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace). /// - public byte InterlaceMethod { get; set; } + public InterlaceMode InterlaceMethod { get; set; } } } diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs index 6ef94fb6f9..c2d0916bc2 100644 --- a/tests/ImageSharp.Tests/FileTestBase.cs +++ b/tests/ImageSharp.Tests/FileTestBase.cs @@ -32,6 +32,7 @@ namespace ImageSharp.Tests // new TestFile(TestImages.Png.Blur), // Perf: Enable for local testing only // new TestFile(TestImages.Png.Indexed), // Perf: Enable for local testing only new TestFile(TestImages.Png.Splash), + new TestFile(TestImages.Png.SplashInterlace), // new TestFile(TestImages.Png.Filter0), // Perf: Enable for local testing only // new TestFile(TestImages.Png.Filter1), // Perf: Enable for local testing only // new TestFile(TestImages.Png.Filter2), // Perf: Enable for local testing only diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a5eebeebe9..23632e2332 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -18,8 +18,9 @@ namespace ImageSharp.Tests public static string Pd => folder + "pd.png"; public static string Blur => folder + "blur.png"; public static string Indexed => folder + "indexed.png"; - public static string Splash => folder + "splash.png"; + public static string Splash => folder + "splash-interlaced.png"; + public static string SplashInterlace => folder + "splash.png"; // filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public static string Filter0 => folder + "filter0.png"; public static string Filter1 => folder + "filter1.png"; diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Png/splash-interlaced.png b/tests/ImageSharp.Tests/TestImages/Formats/Png/splash-interlaced.png new file mode 100644 index 0000000000..98e4517c58 --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Png/splash-interlaced.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a573ae07d97481e8e29441dd165735ad21c0a7bdab1de302d9e442fd6891ec7 +size 239173