Browse Source

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.
af/merge-core
James Jackson-South 10 years ago
parent
commit
b94a865d1c
  1. 23
      src/ImageSharp/Formats/Png/InterlaceMode.cs
  2. 170
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  3. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 2
      src/ImageSharp/Formats/Png/PngHeader.cs
  5. 1
      tests/ImageSharp.Tests/FileTestBase.cs
  6. 3
      tests/ImageSharp.Tests/TestImages.cs
  7. 3
      tests/ImageSharp.Tests/TestImages/Formats/Png/splash-interlaced.png

23
src/ImageSharp/Formats/Png/InterlaceMode.cs

@ -0,0 +1,23 @@
// <copyright file="InterlaceMode.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats
{
/// <summary>
/// Provides enumeration of available PNG interlace modes.
/// </summary>
public enum InterlaceMode : byte
{
/// <summary>
/// Non interlaced
/// </summary>
None,
/// <summary>
/// Adam 7 interlacing.
/// </summary>
Adam7
}
}

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

@ -24,6 +24,26 @@ namespace ImageSharp.Formats
/// </summary>
private static readonly Dictionary<int, byte[]> ColorTypes = new Dictionary<int, byte[]>();
/// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 };
/// <summary>
/// The index to start at when processing each column per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7FirstColumn = { 0, 4, 0, 2, 0, 1, 0 };
/// <summary>
/// The index to start at when processing each row per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7FirstRow = { 0, 0, 4, 0, 2, 0, 1 };
/// <summary>
/// The amount to increment when processing each row per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7RowIncrement = { 8, 8, 8, 4, 4, 2, 2 };
/// <summary>
/// Reusable buffer for reading chunk types.
/// </summary>
@ -285,10 +305,13 @@ namespace ImageSharp.Formats
/// <summary>
/// Calculates the scanline length.
/// </summary>
/// <returns>The <see cref="int"/> representing the length.</returns>
private int CalculateScanlineLength()
/// <param name="width">The width of the row.</param>
/// <returns>
/// The <see cref="int"/> representing the length.
/// </returns>
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
}
}
/// <summary>
/// Decodes the raw interlaced pixel data row by row
/// <see href="https://github.com/juehv/DentalImageViewer/blob/8a1a4424b15d6cc453b5de3f273daf3ff5e3a90d/DentalImageViewer/lib/jiu-0.14.3/net/sourceforge/jiu/codecs/PNGCodec.java"/>
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <typeparam name="TPacked">The packed format. <example>uint, long, float.</example></typeparam>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="pixels">The image pixel accessor.</param>
private void DecodeInterlacedPixelData<TColor, TPacked>(Stream compressedStream, PixelAccessor<TColor, TPacked> pixels)
where TColor : struct, IPackedPixel<TPacked>
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<byte>.Shared.Rent(this.bytesPerScanline);
byte[] scanline = ArrayPool<byte>.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<byte>.Shared.Return(previousScanline);
ArrayPool<byte>.Shared.Return(scanline);
}
y += Adam7RowIncrement[pass];
}
}
}
/// <summary>
/// Processes the de-filtered scanline filling the image pixel data
/// </summary>
@ -406,17 +526,20 @@ namespace ImageSharp.Formats
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="row">The current image row.</param>
/// <param name="pixels">The image pixels</param>
private void ProcessDefilteredScanline<TColor, TPacked>(byte[] defilteredScanline, int row, PixelAccessor<TColor, TPacked> pixels)
/// <param name="startIndex">The column start index. Always 0 for none interlaced images.</param>
/// <param name="increment">The column increment. Always 1 for none interlaced images.</param>
private void ProcessDefilteredScanline<TColor, TPacked>(byte[] defilteredScanline, int row, PixelAccessor<TColor, TPacked> pixels, int startIndex = 0, int increment = 1)
where TColor : struct, IPackedPixel<TPacked>
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];
}
/// <summary>
@ -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}");
}
}
}
}

2
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);
}

2
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).
/// </summary>
public byte InterlaceMethod { get; set; }
public InterlaceMode InterlaceMethod { get; set; }
}
}

1
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

3
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";

3
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
Loading…
Cancel
Save