diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index de494d42d0..d5f8107082 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -5,8 +5,6 @@ namespace ImageSharp.Formats { - using System; - /// /// The Average filter uses the average of the two neighboring pixels (left and above) to predict /// the value of a pixel. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 38042426c8..d5cb40e3fd 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 == PngInterlaceMode.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 + { + byte[] previousScanline = ArrayPool.Shared.Rent(this.bytesPerScanline); + byte[] scanline = ArrayPool.Shared.Rent(this.bytesPerScanline); + + try + { + for (int pass = 0; pass < 7; pass++) + { + // Zero out the previousScanline, because the bytes that are rented from the arraypool may not be zero. + Array.Clear(previousScanline, 0, this.bytesPerScanline); + + 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) + { + 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.ProcessInterlacedDefilteredScanline(scanline, y, pixels, Adam7FirstColumn[pass], Adam7ColumnIncrement[pass]); + + Swap(ref scanline, ref previousScanline); + + y += Adam7RowIncrement[pass]; + } + } + } + finally + { + ArrayPool.Shared.Return(previousScanline); + ArrayPool.Shared.Return(scanline); + } + } + /// /// Processes the de-filtered scanline filling the image pixel data /// @@ -523,6 +643,128 @@ namespace ImageSharp.Formats } } + /// + /// Processes the interlaced de-filtered scanline filling the image pixel data + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The de-filtered scanline + /// The current image row. + /// The image pixels + /// The column start index. Always 0 for none interlaced images. + /// The column increment. Always 1 for none interlaced images. + private void ProcessInterlacedDefilteredScanline(byte[] defilteredScanline, int row, PixelAccessor pixels, int pixelOffset = 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 = pixelOffset; x < this.header.Width; x += increment) + { + byte intensity = (byte)(newScanline1[x - pixelOffset] * factor); + color.PackFromBytes(intensity, intensity, intensity, 255); + pixels[x, row] = color; + } + + break; + + case PngColorType.GrayscaleWithAlpha: + + for (int x = pixelOffset, o = 1; x < this.header.Width; x += increment, o += this.bytesPerPixel) + { + byte intensity = defilteredScanline[o]; + byte alpha = defilteredScanline[o + this.bytesPerSample]; + + color.PackFromBytes(intensity, intensity, intensity, alpha); + pixels[x, row] = color; + } + + break; + + case PngColorType.Palette: + + byte[] newScanline = ToArrayByBitsLength(defilteredScanline, this.bytesPerScanline, this.header.BitDepth); + + if (this.paletteAlpha != null && this.paletteAlpha.Length > 0) + { + // 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 = pixelOffset; x < this.header.Width; x += increment) + { + int index = newScanline[x - pixelOffset]; + int offset = index * 3; + + byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255; + + if (a > 0) + { + byte r = this.palette[offset]; + byte g = this.palette[offset + 1]; + byte b = this.palette[offset + 2]; + color.PackFromBytes(r, g, b, a); + } + else + { + color.PackFromBytes(0, 0, 0, 0); + } + + pixels[x, row] = color; + } + } + else + { + for (int x = pixelOffset; x < this.header.Width; x += increment) + { + int index = newScanline[x - pixelOffset]; + int offset = index * 3; + + byte r = this.palette[offset]; + byte g = this.palette[offset + 1]; + byte b = this.palette[offset + 2]; + + color.PackFromBytes(r, g, b, 255); + pixels[x, row] = color; + } + } + + break; + + case PngColorType.Rgb: + + for (int x = pixelOffset, o = 1; x < this.header.Width; x += increment, o += this.bytesPerPixel) + { + byte r = defilteredScanline[o]; + byte g = defilteredScanline[o + this.bytesPerSample]; + byte b = defilteredScanline[o + (2 * this.bytesPerSample)]; + + color.PackFromBytes(r, g, b, 255); + pixels[x, row] = color; + } + + break; + + case PngColorType.RgbWithAlpha: + + for (int x = pixelOffset, o = 1; x < this.header.Width; x += increment, o += this.bytesPerPixel) + { + byte r = defilteredScanline[o]; + byte g = defilteredScanline[o + this.bytesPerSample]; + byte b = defilteredScanline[o + (2 * this.bytesPerSample)]; + byte a = defilteredScanline[o + (3 * this.bytesPerSample)]; + + color.PackFromBytes(r, g, b, a); + pixels[x, row] = color; + } + + break; + } + } + /// /// Reads a text chunk containing image properties from the data. /// @@ -570,7 +812,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 = (PngInterlaceMode)data[12]; } /// @@ -596,10 +838,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 != PngInterlaceMode.None && this.header.InterlaceMethod != PngInterlaceMode.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 +956,26 @@ namespace ImageSharp.Formats chunk.Length = BitConverter.ToInt32(this.chunkLengthBuffer, 0); } + + /// + /// Returns the correct number of columns for each interlaced pass. + /// + /// Th current pass index + /// The + 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..f1d332c044 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 PngInterlaceMode InterlaceMethod { get; set; } } } diff --git a/src/ImageSharp/Formats/Png/PngInterlaceMode.cs b/src/ImageSharp/Formats/Png/PngInterlaceMode.cs new file mode 100644 index 0000000000..e32e808c1f --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngInterlaceMode.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 PngInterlaceMode : byte + { + /// + /// Non interlaced + /// + None, + + /// + /// Adam 7 interlacing. + /// + Adam7 + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs b/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs index 583191bfc0..14f43b8e69 100644 --- a/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs +++ b/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs @@ -49,10 +49,10 @@ namespace ImageSharp.Tests.Colors new Alpha8(.5F).ToBytes(rgba, 0, ComponentOrder.XYZW); Assert.Equal(rgba, new byte[] { 0, 0, 0, 128 }); - new Alpha8(.5F).ToBytes(rgb, 0, ComponentOrder.ZYX); + new Alpha8(.5F).ToBytes(bgr, 0, ComponentOrder.ZYX); Assert.Equal(bgr, new byte[] { 0, 0, 0 }); - new Alpha8(.5F).ToBytes(rgb, 0, ComponentOrder.ZYXW); + new Alpha8(.5F).ToBytes(bgra, 0, ComponentOrder.ZYXW); Assert.Equal(bgra, new byte[] { 0, 0, 0, 128 }); } @@ -468,7 +468,7 @@ namespace ImageSharp.Tests.Colors r.ToBytes(rgba, 0, ComponentOrder.XYZW); Assert.Equal(rgba, new byte[] { 9, 115, 202, 127 }); - r.PackedValue = 0x7FCA7309; + r.PackedValue = 0xff4af389; r.ToBytes(rgba, 0, ComponentOrder.XYZW); Assert.Equal(rgba, new byte[] { 9, 115, 202, 127 }); } diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs index 6ef94fb6f9..e3aa12460b 100644 --- a/tests/ImageSharp.Tests/FileTestBase.cs +++ b/tests/ImageSharp.Tests/FileTestBase.cs @@ -32,6 +32,8 @@ 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.SplashInterlaced), + new TestFile(TestImages.Png.Interlaced), // 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..fcd8d27cd8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -20,6 +20,10 @@ namespace ImageSharp.Tests public static string Indexed => folder + "indexed.png"; public static string Splash => folder + "splash.png"; + public static string SplashInterlaced => folder + "splash-interlaced.png"; + + public static string Interlaced => folder + "interlaced.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/interlaced.png b/tests/ImageSharp.Tests/TestImages/Formats/Png/interlaced.png new file mode 100644 index 0000000000..c9d76336cc --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Png/interlaced.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21d34b50acf6bf71c68673585a298b8ddce3a908a46c38a6be9530487ebab8dc +size 17949 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