diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 58cfa6a70..d5b5bfd0e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -24,7 +24,14 @@ namespace ImageSharp.Formats /// /// The dictionary of available color types. /// - private static readonly Dictionary ColorTypes = new Dictionary(); + private static readonly Dictionary ColorTypes = new Dictionary() + { + [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8 }, + [PngColorType.Rgb] = new byte[] { 8 }, + [PngColorType.Palette] = new byte[] { 1, 2, 4, 8 }, + [PngColorType.GrayscaleWithAlpha] = new byte[] { 8 }, + [PngColorType.RgbWithAlpha] = new byte[] { 8 }, + }; /// /// The amount to increment when processing each column per scanline for each interlaced pass @@ -122,20 +129,24 @@ namespace ImageSharp.Formats private bool isEndChunkReached; /// - /// Initializes static members of the class. + /// Previous scanline processed /// - static PngDecoderCore() - { - ColorTypes.Add((int)PngColorType.Grayscale, new byte[] { 1, 2, 4, 8 }); - - ColorTypes.Add((int)PngColorType.Rgb, new byte[] { 8 }); + private byte[] previousScanline; - ColorTypes.Add((int)PngColorType.Palette, new byte[] { 1, 2, 4, 8 }); + /// + /// The current scanline that is being processed + /// + private byte[] scanline; - ColorTypes.Add((int)PngColorType.GrayscaleWithAlpha, new byte[] { 8 }); + /// + /// The index of the current scanline being processed + /// + private int currentRow = 0; - ColorTypes.Add((int)PngColorType.RgbWithAlpha, new byte[] { 8 }); - } + /// + /// The current number of bytes read in the current scanline + /// + private int currentRowBytesRead = 0; /// /// Initializes a new instance of the class. @@ -171,65 +182,76 @@ namespace ImageSharp.Formats ImageMetaData metadata = new ImageMetaData(); this.currentStream = stream; this.currentStream.Skip(8); - - using (MemoryStream dataStream = new MemoryStream()) + Image image = null; + PixelAccessor pixels = null; + try { - PngChunk currentChunk; - while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null) + using (DeframeStream deframeStream = new DeframeStream(this.currentStream)) { - try + PngChunk currentChunk; + while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null) { - switch (currentChunk.Type) + try { - case PngChunkTypes.Header: - this.ReadHeaderChunk(currentChunk.Data); - this.ValidateHeader(); - break; - case PngChunkTypes.Physical: - this.ReadPhysicalChunk(metadata, 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; - 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(metadata, currentChunk.Data, currentChunk.Length); - break; - case PngChunkTypes.End: - this.isEndChunkReached = true; - break; + switch (currentChunk.Type) + { + case PngChunkTypes.Header: + this.ReadHeaderChunk(currentChunk.Data); + this.ValidateHeader(); + break; + case PngChunkTypes.Physical: + this.ReadPhysicalChunk(metadata, currentChunk.Data); + break; + case PngChunkTypes.Data: + if (image == null) + { + this.InitializeImage(metadata, out image, out pixels); + } + + deframeStream.AllocateNewBytes(currentChunk.Length); + this.ReadScanlines(deframeStream.CompressedStream, pixels); + stream.Read(this.crcBuffer, 0, 4); + break; + case PngChunkTypes.Palette: + byte[] pal = new byte[currentChunk.Length]; + Buffer.BlockCopy(currentChunk.Data, 0, pal, 0, currentChunk.Length); + this.palette = pal; + 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(metadata, currentChunk.Data, currentChunk.Length); + break; + case PngChunkTypes.End: + this.isEndChunkReached = true; + break; + } + } + finally + { + // Data is rented in ReadChunkData() + if (currentChunk.Data != null) + { + ArrayPool.Shared.Return(currentChunk.Data); + } } } - 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 image = Image.Create(this.header.Width, this.header.Height, metadata, this.configuration); - - using (PixelAccessor pixels = image.Lock()) + return image; + } + finally + { + pixels?.Dispose(); + if (this.previousScanline != null) { - this.ReadScanlines(dataStream, pixels); + ArrayPool.Shared.Return(this.previousScanline); + ArrayPool.Shared.Return(this.scanline); } - - return image; } } @@ -293,6 +315,39 @@ namespace ImageSharp.Formats metadata.VerticalResolution = BitConverter.ToInt32(data, 4) / 39.3700787d; } + /// + /// Initializes the image and various buffers needed for processing + /// + /// The type the pixels will be + /// The metadata information for the image + /// The image that we will populate + /// The pixel accessor + private void InitializeImage(ImageMetaData metadata, out Image image, out PixelAccessor pixels) + where TPixel : struct, IPixel + { + 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 = Image.Create(this.header.Width, this.header.Height, metadata, this.configuration); + pixels = image.Lock(); + 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; + } + + this.previousScanline = ArrayPool.Shared.Rent(this.bytesPerScanline); + this.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(this.scanline, 0, this.bytesPerScanline); + Array.Clear(this.previousScanline, 0, this.bytesPerScanline); + } + /// /// Calculates the correct number of bytes per pixel for the given color type. /// @@ -345,28 +400,16 @@ namespace ImageSharp.Formats /// The pixel format. /// The containing data. /// The pixel data. - private void ReadScanlines(MemoryStream dataStream, PixelAccessor pixels) + private void ReadScanlines(Stream dataStream, PixelAccessor pixels) where TPixel : struct, IPixel { - this.bytesPerPixel = this.CalculateBytesPerPixel(); - this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; - this.bytesPerSample = 1; - if (this.header.BitDepth >= 8) + if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) { - this.bytesPerSample = this.header.BitDepth / 8; + this.DecodeInterlacedPixelData(dataStream, pixels); } - - dataStream.Position = 0; - using (ZlibInflateStream compressedStream = new ZlibInflateStream(dataStream)) + else { - if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) - { - this.DecodeInterlacedPixelData(compressedStream, pixels); - } - else - { - this.DecodePixelData(compressedStream, pixels); - } + this.DecodePixelData(dataStream, pixels); } } @@ -379,66 +422,58 @@ namespace ImageSharp.Formats private void DecodePixelData(Stream compressedStream, PixelAccessor pixels) where TPixel : struct, IPixel { - 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 + while (this.currentRow < this.header.Height) { - for (int y = 0; y < this.header.Height; y++) + int bytesRead = compressedStream.Read(this.scanline, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead); + this.currentRowBytesRead += bytesRead; + if (this.currentRowBytesRead < this.bytesPerScanline) { - compressedStream.Read(scanline, 0, this.bytesPerScanline); + return; + } - FilterType filterType = (FilterType)scanline[0]; + this.currentRowBytesRead = 0; + FilterType filterType = (FilterType)this.scanline[0]; - switch (filterType) - { - case FilterType.None: + switch (filterType) + { + case FilterType.None: - NoneFilter.Decode(scanline); + NoneFilter.Decode(this.scanline); - break; + break; - case FilterType.Sub: + case FilterType.Sub: - SubFilter.Decode(scanline, this.bytesPerScanline, this.bytesPerPixel); + SubFilter.Decode(this.scanline, this.bytesPerScanline, this.bytesPerPixel); - break; + break; - case FilterType.Up: + case FilterType.Up: - UpFilter.Decode(scanline, previousScanline, this.bytesPerScanline); + UpFilter.Decode(this.scanline, this.previousScanline, this.bytesPerScanline); - break; + break; - case FilterType.Average: + case FilterType.Average: - AverageFilter.Decode(scanline, previousScanline, this.bytesPerScanline, this.bytesPerPixel); + AverageFilter.Decode(this.scanline, this.previousScanline, this.bytesPerScanline, this.bytesPerPixel); - break; + break; - case FilterType.Paeth: + case FilterType.Paeth: - PaethFilter.Decode(scanline, previousScanline, this.bytesPerScanline, this.bytesPerPixel); + PaethFilter.Decode(this.scanline, this.previousScanline, this.bytesPerScanline, this.bytesPerPixel); - break; + break; - default: - throw new ImageFormatException("Unknown filter type."); - } + default: + throw new ImageFormatException("Unknown filter type."); + } - this.ProcessDefilteredScanline(scanline, y, pixels); + this.ProcessDefilteredScanline(this.scanline, pixels); - Swap(ref scanline, ref previousScanline); - } - } - finally - { - ArrayPool.Shared.Return(previousScanline); - ArrayPool.Shared.Return(scanline); + Swap(ref this.scanline, ref this.previousScanline); + this.currentRow++; } } @@ -536,12 +571,13 @@ namespace ImageSharp.Formats /// /// The pixel format. /// The de-filtered scanline - /// The current image row. /// The image pixels - private void ProcessDefilteredScanline(byte[] defilteredScanline, int row, PixelAccessor pixels) + private void ProcessDefilteredScanline(byte[] defilteredScanline, PixelAccessor pixels) where TPixel : struct, IPixel { TPixel color = default(TPixel); + BufferSpan pixelBuffer = pixels.GetRowSpan(this.currentRow); + BufferSpan scanlineBuffer = new BufferSpan(defilteredScanline, 1); switch (this.PngColorType) { case PngColorType.Grayscale: @@ -551,7 +587,7 @@ namespace ImageSharp.Formats { byte intensity = (byte)(newScanline1[x] * factor); color.PackFromBytes(intensity, intensity, intensity, 255); - pixels[x, row] = color; + pixels[x, this.currentRow] = color; } break; @@ -566,91 +602,84 @@ namespace ImageSharp.Formats byte alpha = defilteredScanline[offset + this.bytesPerSample]; color.PackFromBytes(intensity, intensity, intensity, alpha); - pixels[x, row] = color; + pixels[x, this.currentRow] = color; } break; case PngColorType.Palette: - byte[] newScanline = ToArrayByBitsLength(defilteredScanline, this.bytesPerScanline, this.header.BitDepth); + this.ProcessScanlineFromPalette(defilteredScanline, pixels); - 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 + 1]; - int pixelOffset = index * 3; + break; - byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255; + case PngColorType.Rgb: - 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); - } + BulkPixelOperations.Instance.PackFromXyzBytes(scanlineBuffer, pixelBuffer, this.header.Width); - pixels[x, row] = color; - } - } - else - { - for (int x = 0; x < this.header.Width; x++) - { - int index = newScanline[x + 1]; - int pixelOffset = index * 3; + break; - byte r = this.palette[pixelOffset]; - byte g = this.palette[pixelOffset + 1]; - byte b = this.palette[pixelOffset + 2]; + case PngColorType.RgbWithAlpha: - color.PackFromBytes(r, g, b, 255); - pixels[x, row] = color; - } - } + BulkPixelOperations.Instance.PackFromXyzwBytes(scanlineBuffer, pixelBuffer, this.header.Width); break; + } + } - case PngColorType.Rgb: + /// + /// Processes a scanline that uses a palette + /// + /// The type of pixel we are expanding to + /// The scanline + /// The output pixels + private void ProcessScanlineFromPalette(byte[] defilteredScanline, PixelAccessor pixels) + where TPixel : struct, IPixel + { + byte[] newScanline = ToArrayByBitsLength(defilteredScanline, this.bytesPerScanline, this.header.BitDepth); + byte[] palette = this.palette; + TPixel color = default(TPixel); - for (int x = 0; x < this.header.Width; x++) - { - int offset = 1 + (x * this.bytesPerPixel); + 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 + 1]; + int pixelOffset = index * 3; - byte r = defilteredScanline[offset]; - byte g = defilteredScanline[offset + this.bytesPerSample]; - byte b = defilteredScanline[offset + (2 * this.bytesPerSample)]; + byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255; - color.PackFromBytes(r, g, b, 255); - pixels[x, row] = color; + if (a > 0) + { + byte r = palette[pixelOffset]; + byte g = palette[pixelOffset + 1]; + byte b = palette[pixelOffset + 2]; + color.PackFromBytes(r, g, b, a); } - - break; - - case PngColorType.RgbWithAlpha: - - for (int x = 0; x < this.header.Width; x++) + else { - int offset = 1 + (x * this.bytesPerPixel); + color.PackFromBytes(0, 0, 0, 0); + } - byte r = defilteredScanline[offset]; - byte g = defilteredScanline[offset + this.bytesPerSample]; - byte b = defilteredScanline[offset + (2 * this.bytesPerSample)]; - byte a = defilteredScanline[offset + (3 * this.bytesPerSample)]; + pixels[x, this.currentRow] = color; + } + } + else + { + for (int x = 0; x < this.header.Width; x++) + { + int index = newScanline[x + 1]; + int pixelOffset = index * 3; - color.PackFromBytes(r, g, b, a); - pixels[x, row] = color; - } + byte r = palette[pixelOffset]; + byte g = palette[pixelOffset + 1]; + byte b = palette[pixelOffset + 2]; - break; + color.PackFromBytes(r, g, b, 255); + pixels[x, this.currentRow] = color; + } } } @@ -819,7 +848,7 @@ namespace ImageSharp.Formats this.header.Height = BitConverter.ToInt32(data, 4); this.header.BitDepth = data[8]; - this.header.ColorType = data[9]; + this.header.ColorType = (PngColorType)data[9]; this.header.CompressionMethod = data[10]; this.header.FilterMethod = data[11]; this.header.InterlaceMethod = (PngInterlaceMode)data[12]; @@ -872,6 +901,11 @@ namespace ImageSharp.Formats } this.ReadChunkType(chunk); + if (chunk.Type == PngChunkTypes.Data) + { + return chunk; + } + this.ReadChunkData(chunk); this.ReadChunkCrc(chunk); diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e30f9791e..31e8cd90e 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -181,7 +181,7 @@ namespace ImageSharp.Formats { Width = image.Width, Height = image.Height, - ColorType = (byte)this.pngColorType, + ColorType = this.pngColorType, BitDepth = this.bitDepth, FilterMethod = 0, // None CompressionMethod = 0, @@ -462,7 +462,7 @@ namespace ImageSharp.Formats WriteInteger(this.chunkDataBuffer, 4, header.Height); this.chunkDataBuffer[8] = header.BitDepth; - this.chunkDataBuffer[9] = header.ColorType; + this.chunkDataBuffer[9] = (byte)header.ColorType; this.chunkDataBuffer[10] = header.CompressionMethod; this.chunkDataBuffer[11] = header.FilterMethod; this.chunkDataBuffer[12] = (byte)header.InterlaceMethod; diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/PngHeader.cs index f1d332c04..50d6cc9ec 100644 --- a/src/ImageSharp/Formats/Png/PngHeader.cs +++ b/src/ImageSharp/Formats/Png/PngHeader.cs @@ -34,7 +34,7 @@ namespace ImageSharp.Formats /// image data. Color type codes represent sums of the following values: /// 1 (palette used), 2 (color used), and 4 (alpha channel used). /// - public byte ColorType { get; set; } + public PngColorType ColorType { get; set; } /// /// Gets or sets the compression method. diff --git a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs index 5f92cc9e0..e5101532c 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs +++ b/src/ImageSharp/Formats/Png/Zlib/Adler32.cs @@ -132,18 +132,13 @@ namespace ImageSharp.Formats throw new ArgumentOutOfRangeException(nameof(count), "cannot be negative"); } - if (offset >= buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset), "not a valid index into buffer"); - } - if (offset + count > buffer.Length) { throw new ArgumentOutOfRangeException(nameof(count), "exceeds buffer size"); } // (By Per Bothner) - uint s1 = this.checksum & 0xFFFF; + uint s1 = this.checksum; uint s2 = this.checksum >> 16; while (count > 0) @@ -151,16 +146,12 @@ namespace ImageSharp.Formats // We can defer the modulo operation: // s1 maximally grows from 65521 to 65521 + 255 * 3800 // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 - int n = 3800; - if (n > count) - { - n = count; - } + int n = Math.Min(3800, count); count -= n; - while (--n >= 0) + while (--n > -1) { - s1 = s1 + (uint)(buffer[offset++] & 0xff); + s1 = s1 + buffer[offset++]; s2 = s2 + s1; } diff --git a/src/ImageSharp/Formats/Png/Zlib/DeframeStream.cs b/src/ImageSharp/Formats/Png/Zlib/DeframeStream.cs new file mode 100644 index 000000000..9b0a61b67 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Zlib/DeframeStream.cs @@ -0,0 +1,121 @@ +namespace ImageSharp.Formats +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + + /// + /// Provides methods and properties for deframing streams from PNGs. + /// + internal class DeframeStream : Stream + { + /// + /// The inner raw memory stream + /// + private readonly Stream innerStream; + + /// + /// The compressed stream sitting over the top of the deframer + /// + private ZlibInflateStream compressedStream; + + /// + /// The current data remaining to be read + /// + private int currentDataRemaining; + + /// + /// Initializes a new instance of the class. + /// + /// The inner raw stream + public DeframeStream(Stream innerStream) + { + this.innerStream = innerStream; + } + + /// + public override bool CanRead => this.innerStream.CanRead; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => throw new NotSupportedException(); + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + /// + /// Gets the compressed stream over the deframed inner stream + /// + public ZlibInflateStream CompressedStream => this.compressedStream; + + /// + /// Adds new bytes from a frame found in the original stream + /// + /// blabla + public void AllocateNewBytes(int bytes) + { + this.currentDataRemaining = bytes; + if (this.compressedStream == null) + { + this.compressedStream = new ZlibInflateStream(this); + } + } + + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + public override int ReadByte() + { + this.currentDataRemaining--; + return this.innerStream.ReadByte(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (this.currentDataRemaining == 0) + { + return 0; + } + + int bytesToRead = Math.Min(count, this.currentDataRemaining); + this.currentDataRemaining -= bytesToRead; + return this.innerStream.Read(buffer, offset, bytesToRead); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + this.compressedStream.Dispose(); + base.Dispose(disposing); + } + } +}