From 5e56b16c099b049f005784848714b1e56da17cbf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 8 Nov 2016 01:45:01 +1100 Subject: [PATCH] Improve png speed and half memory usage --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 172 +++++++++++------- .../General/ArrayReverse.cs | 61 +++++++ 2 files changed, 166 insertions(+), 67 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/General/ArrayReverse.cs diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 2941a43f54..12bc040f5f 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -20,6 +20,26 @@ namespace ImageSharp.Formats /// private static readonly Dictionary ColorTypes = new Dictionary(); + /// + /// 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]; + /// /// The stream to decode from. /// @@ -148,9 +168,12 @@ namespace ImageSharp.Formats + $"max allowed size '{image.MaxWidth}x{image.MaxHeight}'"); } - TColor[] pixels = new TColor[this.header.Width * this.header.Height]; - this.ReadScanlines(dataStream, pixels); - image.SetPixels(this.header.Width, this.header.Height, pixels); + image.InitPixels(this.header.Width, this.header.Height); + + using (PixelAccessor pixels = image.Lock()) + { + this.ReadScanlines(dataStream, pixels); + } } } @@ -165,8 +188,8 @@ namespace ImageSharp.Formats where TColor : struct, IPackedPixel where TPacked : struct { - Array.Reverse(data, 0, 4); - Array.Reverse(data, 4, 4); + this.ReverseBytes(data, 0, 4); + this.ReverseBytes(data, 4, 4); // 39.3700787 = inches in a meter. image.HorizontalResolution = BitConverter.ToInt32(data, 0) / 39.3700787d; @@ -194,8 +217,7 @@ namespace ImageSharp.Formats return 3; // PngColorType.RgbWithAlpha - // TODO: Maybe figure out a way to detect if there are any transparent - // pixels and encode RGB if none. + // TODO: Maybe figure out a way to detect if there are any transparent pixels and encode RGB if none. default: return 4; } @@ -225,7 +247,7 @@ namespace ImageSharp.Formats /// The packed format. uint, long, float. /// The containing data. /// The pixel data. - private void ReadScanlines(MemoryStream dataStream, TColor[] pixels) + private void ReadScanlines(MemoryStream dataStream, PixelAccessor pixels) where TColor : struct, IPackedPixel where TPacked : struct { @@ -244,10 +266,14 @@ namespace ImageSharp.Formats { compressedStream.CopyTo(decompressedStream); decompressedStream.Flush(); - - byte[] decompressedBytes = decompressedStream.ToArray(); - this.DecodePixelData(decompressedBytes, pixels); + decompressedStream.Position = 0; + this.DecodePixelData(decompressedStream, pixels); + //byte[] decompressedBytes = decompressedStream.ToArray(); + //this.DecodePixelData(decompressedBytes, pixels); } + + //byte[] decompressedBytes = compressedStream.ToArray(); + //this.DecodePixelData(decompressedBytes, pixels); } } @@ -258,16 +284,17 @@ namespace ImageSharp.Formats /// The packed format. uint, long, float. /// The pixel data. /// The image pixels. - private void DecodePixelData(byte[] pixelData, TColor[] pixels) + private void DecodePixelData(Stream pixelData, PixelAccessor pixels) where TColor : struct, IPackedPixel where TPacked : struct { + // TODO: ArrayPool.Shared.Rent(this.bytesPerScanline) byte[] previousScanline = new byte[this.bytesPerScanline]; - + byte[] scanline = new byte[this.bytesPerScanline]; for (int y = 0; y < this.header.Height; y++) { - byte[] scanline = new byte[this.bytesPerScanline]; - Array.Copy(pixelData, y * this.bytesPerScanline, scanline, 0, this.bytesPerScanline); + pixelData.Read(scanline, 0, this.bytesPerScanline); + FilterType filterType = (FilterType)scanline[0]; byte[] defilteredScanline; @@ -308,7 +335,7 @@ namespace ImageSharp.Formats } previousScanline = defilteredScanline; - this.ProcessDefilteredScanline(defilteredScanline, y, pixels); + this.ProcessDefilteredScanline(defilteredScanline, y, pixels); } } @@ -320,10 +347,11 @@ namespace ImageSharp.Formats /// The de-filtered scanline /// The current image row. /// The image pixels - private void ProcessDefilteredScanline(byte[] defilteredScanline, int row, TColor[] pixels) + private void ProcessDefilteredScanline(byte[] defilteredScanline, int row, PixelAccessor pixels) where TColor : struct, IPackedPixel where TPacked : struct { + TColor color = default(TColor); switch (this.PngColorType) { case PngColorType.Grayscale: @@ -333,10 +361,8 @@ namespace ImageSharp.Formats int offset = 1 + (x * this.bytesPerPixel); byte intensity = defilteredScanline[offset]; - - TColor color = default(TColor); color.PackFromBytes(intensity, intensity, intensity, 255); - pixels[(row * this.header.Width) + x] = color; + pixels[x, row] = color; } break; @@ -350,9 +376,8 @@ namespace ImageSharp.Formats byte intensity = defilteredScanline[offset]; byte alpha = defilteredScanline[offset + this.bytesPerSample]; - TColor color = default(TColor); color.PackFromBytes(intensity, intensity, intensity, alpha); - pixels[(row * this.header.Width) + x] = color; + pixels[x, row] = color; } break; @@ -365,14 +390,13 @@ 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 i = 0; i < this.header.Width; i++) + for (int x = 0; x < this.header.Width; x++) { - int index = newScanline[i]; - int offset = (row * this.header.Width) + i; + int index = newScanline[x]; int pixelOffset = index * 3; byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255; - TColor color = default(TColor); + if (a > 0) { byte r = this.palette[pixelOffset]; @@ -381,24 +405,22 @@ namespace ImageSharp.Formats color.PackFromBytes(r, g, b, a); } - pixels[offset] = color; + pixels[x, row] = color; } } else { - for (int i = 0; i < this.header.Width; i++) + for (int x = 0; x < this.header.Width; x++) { - int index = newScanline[i]; - int offset = (row * this.header.Width) + i; + 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]; - TColor color = default(TColor); color.PackFromBytes(r, g, b, 255); - pixels[offset] = color; + pixels[x, row] = color; } } @@ -414,9 +436,8 @@ namespace ImageSharp.Formats byte g = defilteredScanline[offset + this.bytesPerSample]; byte b = defilteredScanline[offset + (2 * this.bytesPerSample)]; - TColor color = default(TColor); color.PackFromBytes(r, g, b, 255); - pixels[(row * this.header.Width) + x] = color; + pixels[x, row] = color; } break; @@ -432,9 +453,8 @@ namespace ImageSharp.Formats byte b = defilteredScanline[offset + (2 * this.bytesPerSample)]; byte a = defilteredScanline[offset + (3 * this.bytesPerSample)]; - TColor color = default(TColor); color.PackFromBytes(r, g, b, a); - pixels[(row * this.header.Width) + x] = color; + pixels[x, row] = color; } break; @@ -477,17 +497,17 @@ namespace ImageSharp.Formats { this.header = new PngHeader(); - Array.Reverse(data, 0, 4); - Array.Reverse(data, 4, 4); + this.ReverseBytes(data, 0, 4); + this.ReverseBytes(data, 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 = data[12]; - this.header.CompressionMethod = data[10]; } /// @@ -542,10 +562,9 @@ namespace ImageSharp.Formats return null; } - byte[] typeBuffer = this.ReadChunkType(chunk); - + this.ReadChunkType(chunk); this.ReadChunkData(chunk); - this.ReadChunkCrc(chunk, typeBuffer); + this.ReadChunkCrc(chunk); return chunk; } @@ -554,26 +573,23 @@ namespace ImageSharp.Formats /// Reads the cycle redundancy chunk from the data. /// /// The chunk. - /// The type buffer. /// /// Thrown if the input stream is not valid or corrupt. /// - private void ReadChunkCrc(PngChunk chunk, byte[] typeBuffer) + private void ReadChunkCrc(PngChunk chunk) { - byte[] crcBuffer = new byte[4]; - - int numBytes = this.currentStream.Read(crcBuffer, 0, 4); + int numBytes = this.currentStream.Read(this.crcBuffer, 0, 4); if (numBytes >= 1 && numBytes <= 3) { throw new ImageFormatException("Image stream is not valid!"); } - Array.Reverse(crcBuffer); + this.ReverseBytes(this.crcBuffer); - chunk.Crc = BitConverter.ToUInt32(crcBuffer, 0); + chunk.Crc = BitConverter.ToUInt32(this.crcBuffer, 0); Crc32 crc = new Crc32(); - crc.Update(typeBuffer); + crc.Update(this.chunkTypeBuffer); crc.Update(chunk.Data); if (crc.Value != chunk.Crc) @@ -602,25 +618,20 @@ namespace ImageSharp.Formats /// /// Thrown if the input stream is not valid. /// - private byte[] ReadChunkType(PngChunk chunk) + private void ReadChunkType(PngChunk chunk) { - byte[] typeBuffer = new byte[4]; - - int numBytes = this.currentStream.Read(typeBuffer, 0, 4); + int numBytes = this.currentStream.Read(this.chunkTypeBuffer, 0, 4); if (numBytes >= 1 && numBytes <= 3) { throw new ImageFormatException("Image stream is not valid!"); } - char[] chars = new char[4]; - chars[0] = (char)typeBuffer[0]; - chars[1] = (char)typeBuffer[1]; - chars[2] = (char)typeBuffer[2]; - chars[3] = (char)typeBuffer[3]; - - chunk.Type = new string(chars); + 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]; - return typeBuffer; + chunk.Type = new string(this.chars); } /// @@ -635,19 +646,46 @@ namespace ImageSharp.Formats /// private int ReadChunkLength(PngChunk chunk) { - byte[] lengthBuffer = new byte[4]; - - int numBytes = this.currentStream.Read(lengthBuffer, 0, 4); + int numBytes = this.currentStream.Read(this.chunkLengthBuffer, 0, 4); if (numBytes >= 1 && numBytes <= 3) { throw new ImageFormatException("Image stream is not valid!"); } - Array.Reverse(lengthBuffer); + this.ReverseBytes(this.chunkLengthBuffer); - chunk.Length = BitConverter.ToInt32(lengthBuffer, 0); + chunk.Length = BitConverter.ToInt32(this.chunkLengthBuffer, 0); return numBytes; } + + /// + /// Optimized reversal algorithm. + /// + /// The byte array. + private void ReverseBytes(byte[] source) + { + this.ReverseBytes(source, 0, source.Length); + } + + /// + /// Optimized reversal algorithm. + /// + /// The byte array. + /// The index. + /// The length. + private void ReverseBytes(byte[] source, int index, int length) + { + int i = index; + int j = index + length - 1; + while (i < j) + { + byte temp = source[i]; + source[i] = source[j]; + source[j] = temp; + i++; + j--; + } + } } } diff --git a/tests/ImageSharp.Benchmarks/General/ArrayReverse.cs b/tests/ImageSharp.Benchmarks/General/ArrayReverse.cs new file mode 100644 index 0000000000..970f6b347c --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/ArrayReverse.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Benchmarks.General +{ + using System; + + using BenchmarkDotNet.Attributes; + + public class ArrayReverse + { + [Params(4, 16, 32)] + public int Count { get; set; } + + byte[] source; + + byte[] destination; + + [Setup] + public void SetUp() + { + this.source = new byte[this.Count]; + this.destination = new byte[this.Count]; + } + + [Benchmark(Baseline = true, Description = "Copy using Array.Reverse()")] + public void ReverseArray() + { + Array.Reverse(this.source, 0, this.Count); + } + + [Benchmark(Description = "Reverse using loop")] + public void ReverseLoop() + { + this.ReverseBytes(this.source, 0, this.Count); + + //for (int i = 0; i < this.source.Length / 2; i++) + //{ + // byte tmp = this.source[i]; + // this.source[i] = this.source[this.source.Length - i - 1]; + // this.source[this.source.Length - i - 1] = tmp; + //} + } + + public void ReverseBytes(byte[] source, int index, int length) + { + int i = index; + int j = index + length - 1; + while (i < j) + { + byte temp = source[i]; + source[i] = source[j]; + source[j] = temp; + i++; + j--; + } + } + } +}