//
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
//
namespace ImageSharp.Formats
{
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using static ComparableExtensions;
///
/// Performs the png decoding operation.
///
internal class PngDecoderCore
{
///
/// The dictionary of available color types.
///
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.
///
private readonly byte[] chunkTypeBuffer = new byte[4];
///
/// Reusable buffer for reading chunk lengths.
///
private readonly byte[] chunkLengthBuffer = new byte[4];
///
/// Reusable buffer for reading crc values.
///
private readonly byte[] crcBuffer = new byte[4];
///
/// Reusable buffer for reading char arrays.
///
private readonly char[] chars = new char[4];
///
/// Reusable crc for validating chunks.
///
private readonly Crc32 crc = new Crc32();
///
/// The stream to decode from.
///
private Stream currentStream;
///
/// The png header.
///
private PngHeader header;
///
/// The number of bytes per pixel.
///
private int bytesPerPixel;
///
/// The number of bytes per sample
///
private int bytesPerSample;
///
/// The number of bytes per scanline
///
private int bytesPerScanline;
///
/// The palette containing color information for indexed png's
///
private byte[] palette;
///
/// The palette containing alpha channel color information for indexed png's
///
private byte[] paletteAlpha;
///
/// Initializes static members of the class.
///
static PngDecoderCore()
{
ColorTypes.Add((int)PngColorType.Grayscale, new byte[] { 1, 2, 4, 8 });
ColorTypes.Add((int)PngColorType.Rgb, new byte[] { 8 });
ColorTypes.Add((int)PngColorType.Palette, new byte[] { 1, 2, 4, 8 });
ColorTypes.Add((int)PngColorType.GrayscaleWithAlpha, new byte[] { 8 });
ColorTypes.Add((int)PngColorType.RgbWithAlpha, new byte[] { 8 });
}
///
/// Gets or sets the png color type
///
public PngColorType PngColorType { get; set; }
///
/// Decodes the stream to the image.
///
/// The pixel format.
/// The image to decode to.
/// The stream containing image data.
///
/// Thrown if the stream does not contain and end chunk.
///
///
/// Thrown if the image is larger than the maximum allowable size.
///
public void Decode(Image image, Stream stream)
where TColor : struct, IPackedPixel, IEquatable
{
Image currentImage = image;
this.currentStream = stream;
this.currentStream.Skip(8);
bool isEndChunkReached = false;
using (MemoryStream dataStream = new MemoryStream())
{
PngChunk currentChunk;
while ((currentChunk = this.ReadChunk()) != null)
{
if (isEndChunkReached)
{
throw new ImageFormatException("Image does not end with end chunk.");
}
try
{
switch (currentChunk.Type)
{
case PngChunkTypes.Header:
this.ReadHeaderChunk(currentChunk.Data);
this.ValidateHeader();
break;
case PngChunkTypes.Physical:
this.ReadPhysicalChunk(currentImage, currentChunk.Data);
break;
case PngChunkTypes.Data:
dataStream.Write(currentChunk.Data, 0, currentChunk.Length);
break;
case PngChunkTypes.Palette:
byte[] pal = new byte[currentChunk.Length];
Buffer.BlockCopy(currentChunk.Data, 0, pal, 0, currentChunk.Length);
this.palette = pal;
image.MetaData.Quality = pal.Length / 3;
break;
case PngChunkTypes.PaletteAlpha:
byte[] alpha = new byte[currentChunk.Length];
Buffer.BlockCopy(currentChunk.Data, 0, alpha, 0, currentChunk.Length);
this.paletteAlpha = alpha;
break;
case PngChunkTypes.Text:
this.ReadTextChunk(currentImage, currentChunk.Data, currentChunk.Length);
break;
case PngChunkTypes.End:
isEndChunkReached = true;
break;
}
}
finally
{
// Data is rented in ReadChunkData()
ArrayPool.Shared.Return(currentChunk.Data);
}
}
if (this.header.Width > image.MaxWidth || this.header.Height > image.MaxHeight)
{
throw new ArgumentOutOfRangeException($"The input png '{this.header.Width}x{this.header.Height}' is bigger than the max allowed size '{image.MaxWidth}x{image.MaxHeight}'");
}
image.InitPixels(this.header.Width, this.header.Height);
using (PixelAccessor pixels = image.Lock())
{
this.ReadScanlines(dataStream, pixels);
}
}
}
///
/// Converts a byte array to a new array where each value in the original array is represented by the specified number of bits.
///
/// The bytes to convert from. Cannot be null.
/// The number of bytes per scanline
/// The number of bits per value.
/// The resulting array. Is never null.
/// is null.
/// is less than or equals than zero.
private static byte[] ToArrayByBitsLength(byte[] source, int bytesPerScanline, int bits)
{
Guard.NotNull(source, nameof(source));
Guard.MustBeGreaterThan(bits, 0, nameof(bits));
byte[] result;
if (bits < 8)
{
result = new byte[bytesPerScanline * 8 / bits];
int mask = 0xFF >> (8 - bits);
int resultOffset = 0;
// ReSharper disable once ForCanBeConvertedToForeach
// First byte is the marker so skip.
for (int i = 1; i < bytesPerScanline; i++)
{
byte b = source[i];
for (int shift = 0; shift < 8; shift += bits)
{
int colorIndex = (b >> (8 - bits - shift)) & mask;
result[resultOffset] = (byte)colorIndex;
resultOffset++;
}
}
}
else
{
result = source;
}
return result;
}
///
/// Reads the data chunk containing physical dimension data.
///
/// The pixel format.
/// The image to read to.
/// The data containing physical data.
private void ReadPhysicalChunk(Image image, byte[] data)
where TColor : struct, IPackedPixel, IEquatable
{
data.ReverseBytes(0, 4);
data.ReverseBytes(4, 4);
// 39.3700787 = inches in a meter.
image.MetaData.HorizontalResolution = BitConverter.ToInt32(data, 0) / 39.3700787d;
image.MetaData.VerticalResolution = BitConverter.ToInt32(data, 4) / 39.3700787d;
}
///
/// Calculates the correct number of bytes per pixel for the given color type.
///
/// The
private int CalculateBytesPerPixel()
{
switch (this.PngColorType)
{
case PngColorType.Grayscale:
return 1;
case PngColorType.GrayscaleWithAlpha:
return 2;
case PngColorType.Palette:
return 1;
case PngColorType.Rgb:
return 3;
// PngColorType.RgbWithAlpha:
default:
return 4;
}
}
///
/// Calculates the scanline length.
///
/// The width of the row.
///
/// The representing the length.
///
private int CalculateScanlineLength(int width)
{
int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel;
int amount = scanlineLength % 8;
if (amount != 0)
{
scanlineLength += 8 - amount;
}
return scanlineLength / 8;
}
///
/// Reads the scanlines within the image.
///
/// The pixel format.
/// The containing data.
/// The pixel data.
private void ReadScanlines(MemoryStream dataStream, PixelAccessor pixels)
where TColor : struct, IPackedPixel, IEquatable
{
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
if (this.header.BitDepth >= 8)
{
this.bytesPerSample = this.header.BitDepth / 8;
}
dataStream.Position = 0;
using (ZlibInflateStream compressedStream = new ZlibInflateStream(dataStream))
{
if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
{
this.DecodeInterlacedPixelData(compressedStream, pixels);
}
else
{
this.DecodePixelData(compressedStream, pixels);
}
}
}
///
/// Decodes the raw pixel data row by row
///
/// The pixel format.
/// The compressed pixel data stream.
/// The image pixel accessor.
private void DecodePixelData(Stream compressedStream, PixelAccessor pixels)
where TColor : struct, IPackedPixel, IEquatable
{
byte[] previousScanline = ArrayPool.Shared.Rent(this.bytesPerScanline);
byte[] scanline = ArrayPool.Shared.Rent(this.bytesPerScanline);
// Zero out the scanlines, because the bytes that are rented from the arraypool may not be zero.
Array.Clear(scanline, 0, this.bytesPerScanline);
Array.Clear(previousScanline, 0, this.bytesPerScanline);
try
{
for (int y = 0; y < this.header.Height; y++)
{
compressedStream.Read(scanline, 0, this.bytesPerScanline);
FilterType filterType = (FilterType)scanline[0];
switch (filterType)
{
case FilterType.None:
NoneFilter.Decode(scanline);
break;
case FilterType.Sub:
SubFilter.Decode(scanline, this.bytesPerScanline, this.bytesPerPixel);
break;
case FilterType.Up:
UpFilter.Decode(scanline, previousScanline, this.bytesPerScanline);
break;
case FilterType.Average:
AverageFilter.Decode(scanline, previousScanline, this.bytesPerScanline, this.bytesPerPixel);
break;
case FilterType.Paeth:
PaethFilter.Decode(scanline, previousScanline, this.bytesPerScanline, this.bytesPerPixel);
break;
default:
throw new ImageFormatException("Unknown filter type.");
}
this.ProcessDefilteredScanline(scanline, y, pixels);
Swap(ref scanline, ref previousScanline);
}
}
finally
{
ArrayPool.Shared.Return(previousScanline);
ArrayPool.Shared.Return(scanline);
}
}
///
/// Decodes the raw interlaced pixel data row by row
///
///
/// The pixel format.
/// The compressed pixel data stream.
/// The image pixel accessor.
private void DecodeInterlacedPixelData(Stream compressedStream, PixelAccessor pixels)
where TColor : struct, IPackedPixel, IEquatable
{
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 scanlines, because the bytes that are rented from the arraypool may not be zero.
Array.Clear(scanline, 0, this.bytesPerScanline);
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
///
/// The pixel format.
/// The de-filtered scanline
/// The current image row.
/// The image pixels
private void ProcessDefilteredScanline(byte[] defilteredScanline, int row, PixelAccessor pixels)
where TColor : struct, IPackedPixel, IEquatable
{
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++)
{
byte intensity = (byte)(newScanline1[x] * factor);
color.PackFromBytes(intensity, intensity, intensity, 255);
pixels[x, row] = color;
}
break;
case PngColorType.GrayscaleWithAlpha:
for (int x = 0; x < this.header.Width; x++)
{
int offset = 1 + (x * this.bytesPerPixel);
byte intensity = defilteredScanline[offset];
byte alpha = defilteredScanline[offset + 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 = 0; x < this.header.Width; x++)
{
int index = newScanline[x];
int pixelOffset = index * 3;
byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255;
if (a > 0)
{
byte r = this.palette[pixelOffset];
byte g = this.palette[pixelOffset + 1];
byte b = this.palette[pixelOffset + 2];
color.PackFromBytes(r, g, b, a);
}
else
{
color.PackFromBytes(0, 0, 0, 0);
}
pixels[x, row] = color;
}
}
else
{
for (int x = 0; x < this.header.Width; x++)
{
int index = newScanline[x];
int pixelOffset = index * 3;
byte r = this.palette[pixelOffset];
byte g = this.palette[pixelOffset + 1];
byte b = this.palette[pixelOffset + 2];
color.PackFromBytes(r, g, b, 255);
pixels[x, row] = color;
}
}
break;
case PngColorType.Rgb:
for (int x = 0; x < this.header.Width; x++)
{
int offset = 1 + (x * this.bytesPerPixel);
byte r = defilteredScanline[offset];
byte g = defilteredScanline[offset + this.bytesPerSample];
byte b = defilteredScanline[offset + (2 * this.bytesPerSample)];
color.PackFromBytes(r, g, b, 255);
pixels[x, row] = color;
}
break;
case PngColorType.RgbWithAlpha:
for (int x = 0; x < this.header.Width; x++)
{
int offset = 1 + (x * this.bytesPerPixel);
byte r = defilteredScanline[offset];
byte g = defilteredScanline[offset + this.bytesPerSample];
byte b = defilteredScanline[offset + (2 * this.bytesPerSample)];
byte a = defilteredScanline[offset + (3 * this.bytesPerSample)];
color.PackFromBytes(r, g, b, a);
pixels[x, row] = color;
}
break;
}
}
///
/// Processes the interlaced de-filtered scanline filling the image pixel data
///
/// The pixel format.
/// 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, IEquatable
{
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, o = 1; x < this.header.Width; x += increment, o++)
{
byte intensity = (byte)(newScanline1[o] * 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, o = 1; x < this.header.Width; x += increment, o++)
{
int index = newScanline[o];
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, o = 1; x < this.header.Width; x += increment, o++)
{
int index = newScanline[o];
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.
///
/// The pixel format.
/// The image to decode to.
/// The containing data.
/// The maximum length to read.
private void ReadTextChunk(Image image, byte[] data, int length)
where TColor : struct, IPackedPixel, IEquatable
{
int zeroIndex = 0;
for (int i = 0; i < length; i++)
{
if (data[i] == 0)
{
zeroIndex = i;
break;
}
}
string name = Encoding.Unicode.GetString(data, 0, zeroIndex);
string value = Encoding.Unicode.GetString(data, zeroIndex + 1, length - zeroIndex - 1);
image.MetaData.Properties.Add(new ImageProperty(name, value));
}
///
/// Reads a header chunk from the data.
///
/// The containing data.
private void ReadHeaderChunk(byte[] data)
{
this.header = new PngHeader();
data.ReverseBytes(0, 4);
data.ReverseBytes(4, 4);
this.header.Width = BitConverter.ToInt32(data, 0);
this.header.Height = BitConverter.ToInt32(data, 4);
this.header.BitDepth = data[8];
this.header.ColorType = data[9];
this.header.CompressionMethod = data[10];
this.header.FilterMethod = data[11];
this.header.InterlaceMethod = (PngInterlaceMode)data[12];
}
///
/// Validates the png header.
///
///
/// Thrown if the image does pass validation.
///
private void ValidateHeader()
{
if (!ColorTypes.ContainsKey(this.header.ColorType))
{
throw new NotSupportedException("Color type is not supported or not valid.");
}
if (!ColorTypes[this.header.ColorType].Contains(this.header.BitDepth))
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
if (this.header.FilterMethod != 0)
{
throw new NotSupportedException("The png specification only defines 0 as filter method.");
}
if (this.header.InterlaceMethod != PngInterlaceMode.None && this.header.InterlaceMethod != PngInterlaceMode.Adam7)
{
throw new NotSupportedException("The png specification only defines 'None' and 'Adam7' as interlaced methods.");
}
this.PngColorType = (PngColorType)this.header.ColorType;
}
///
/// Reads a chunk from the stream.
///
///
/// The .
///
private PngChunk ReadChunk()
{
PngChunk chunk = new PngChunk();
this.ReadChunkLength(chunk);
if (chunk.Length < 0)
{
return null;
}
this.ReadChunkType(chunk);
this.ReadChunkData(chunk);
this.ReadChunkCrc(chunk);
return chunk;
}
///
/// Reads the cycle redundancy chunk from the data.
///
/// The chunk.
///
/// Thrown if the input stream is not valid or corrupt.
///
private void ReadChunkCrc(PngChunk chunk)
{
int numBytes = this.currentStream.Read(this.crcBuffer, 0, 4);
if (numBytes >= 1 && numBytes <= 3)
{
throw new ImageFormatException("Image stream is not valid!");
}
this.crcBuffer.ReverseBytes();
chunk.Crc = BitConverter.ToUInt32(this.crcBuffer, 0);
this.crc.Reset();
this.crc.Update(this.chunkTypeBuffer);
this.crc.Update(chunk.Data, 0, chunk.Length);
if (this.crc.Value != chunk.Crc)
{
throw new ImageFormatException("CRC Error. PNG Image chunk is corrupt!");
}
}
///
/// Reads the chunk data from the stream.
///
/// The chunk.
private void ReadChunkData(PngChunk chunk)
{
// We rent the buffer here to return it afterwards in Decode()
chunk.Data = ArrayPool.Shared.Rent(chunk.Length);
this.currentStream.Read(chunk.Data, 0, chunk.Length);
}
///
/// Identifies the chunk type from the chunk.
///
/// The chunk.
///
/// Thrown if the input stream is not valid.
///
private void ReadChunkType(PngChunk chunk)
{
int numBytes = this.currentStream.Read(this.chunkTypeBuffer, 0, 4);
if (numBytes >= 1 && numBytes <= 3)
{
throw new ImageFormatException("Image stream is not valid!");
}
this.chars[0] = (char)this.chunkTypeBuffer[0];
this.chars[1] = (char)this.chunkTypeBuffer[1];
this.chars[2] = (char)this.chunkTypeBuffer[2];
this.chars[3] = (char)this.chunkTypeBuffer[3];
chunk.Type = new string(this.chars);
}
///
/// Calculates the length of the given chunk.
///
/// The chunk.
///
/// Thrown if the input stream is not valid.
///
private void ReadChunkLength(PngChunk chunk)
{
int numBytes = this.currentStream.Read(this.chunkLengthBuffer, 0, 4);
if (numBytes > 1 && numBytes <= 3)
{
throw new ImageFormatException("Image stream is not valid!");
}
if (numBytes <= 1)
{
chunk.Length = -1;
return;
}
this.chunkLengthBuffer.ReverseBytes();
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}");
}
}
}
}