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