From fd11972bb8f37ffa3c8300a20d6d460950f121ed Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 11 Nov 2016 16:51:03 +0100 Subject: [PATCH] allocation optimizations for JpegDecoderCore --- .../Formats/Jpg/Components/Block.cs | 79 +- .../Formats/Jpg/Components/Component.cs | 10 +- src/ImageSharp/Formats/Jpg/Components/FDCT.cs | 2 +- .../Formats/Jpg/Components/Huffman.cs | 65 +- src/ImageSharp/Formats/Jpg/Components/IDCT.cs | 5 +- src/ImageSharp/Formats/Jpg/JpegDecoder.cs | 6 +- src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs | 691 ++++++++++-------- src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs | 63 +- .../Formats/Bmp/BitmapTests.cs | 2 + .../ImageSharp.Tests/Formats/Jpg/JpegTests.cs | 85 +++ .../ImageSharp.Tests/Formats/Png/PngTests.cs | 2 + tests/ImageSharp.Tests/TestImages.cs | 9 +- .../TestImages/Formats/Jpg/geneserath.jpg | 3 + 13 files changed, 651 insertions(+), 371 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Jpg/geneserath.jpg diff --git a/src/ImageSharp/Formats/Jpg/Components/Block.cs b/src/ImageSharp/Formats/Jpg/Components/Block.cs index 8bc5a861c..f41e615b5 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Block.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Block.cs @@ -3,13 +3,19 @@ // Licensed under the Apache License, Version 2.0. // +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + namespace ImageSharp.Formats { /// /// Represents an 8x8 block of coefficients to transform and encode. /// - internal class Block + public struct Block : IDisposable { + private static ArrayPool IntArrayPool = ArrayPool.Create(BlockSize, 50); + /// /// Gets the size of the block. /// @@ -18,16 +24,41 @@ namespace ImageSharp.Formats /// /// The array of block data. /// - private readonly int[] data; + public int[] Data; /// /// Initializes a new instance of the class. /// - public Block() + //public Block() + //{ + // this.data = new int[BlockSize]; + //} + + public void Init() + { + //this.Data = new int[BlockSize]; + this.Data = IntArrayPool.Rent(BlockSize); + } + + public static Block Create() { - this.data = new int[BlockSize]; + var block = new Block(); + block.Init(); + return block; } + public static Block[] CreateArray(int size) + { + Block[] result = new Block[size]; + for (int i = 0; i < result.Length; i++) + { + result[i].Init(); + } + return result; + } + + public bool IsInitialized => this.Data != null; + /// /// Gets the pixel data at the given block index. /// @@ -37,8 +68,44 @@ namespace ImageSharp.Formats /// public int this[int index] { - get { return this.data[index]; } - set { this.data[index] = value; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { return this.Data[index]; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set { this.Data[index] = value; } + } + + // TODO: Refactor Block.Dispose() callers to always use 'using' or 'finally' statement! + public void Dispose() + { + if (Data != null) + { + IntArrayPool.Return(Data, true); + Data = null; + } + } + + public static void DisposeAll(Block[] blocks) + { + for (int i = 0; i < blocks.Length; i++) + { + blocks[i].Dispose(); + } + } + + + public void Clear() + { + for (int i = 0; i < Data.Length; i++) + { + Data[i] = 0; + } + } + + public Block Clone() + { + Block clone = Create(); + Array.Copy(Data, clone.Data, BlockSize); + return clone; } } } diff --git a/src/ImageSharp/Formats/Jpg/Components/Component.cs b/src/ImageSharp/Formats/Jpg/Components/Component.cs index f56b6d513..f70dbff3f 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Component.cs @@ -8,26 +8,26 @@ namespace ImageSharp.Formats /// /// Represents a single color component /// - internal class Component + internal struct Component { /// /// Gets or sets the horizontal sampling factor. /// - public int HorizontalFactor { get; set; } + public int HorizontalFactor; /// /// Gets or sets the vertical sampling factor. /// - public int VerticalFactor { get; set; } + public int VerticalFactor; /// /// Gets or sets the identifier /// - public byte Identifier { get; set; } + public byte Identifier; /// /// Gets or sets the quantization table destination selector. /// - public byte Selector { get; set; } + public byte Selector; } } diff --git a/src/ImageSharp/Formats/Jpg/Components/FDCT.cs b/src/ImageSharp/Formats/Jpg/Components/FDCT.cs index cd27b9e73..13ce80f55 100644 --- a/src/ImageSharp/Formats/Jpg/Components/FDCT.cs +++ b/src/ImageSharp/Formats/Jpg/Components/FDCT.cs @@ -45,7 +45,7 @@ namespace ImageSharp.Formats /// Performs a forward DCT on an 8x8 block of coefficients, including a level shift. /// /// The block of coefficients. - public static void Transform(Block block) + public static void Transform(ref Block block) { // Pass 1: process rows. for (int y = 0; y < 8; y++) diff --git a/src/ImageSharp/Formats/Jpg/Components/Huffman.cs b/src/ImageSharp/Formats/Jpg/Components/Huffman.cs index 2c38cfd38..345b45e0e 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Huffman.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Huffman.cs @@ -3,33 +3,47 @@ // Licensed under the Apache License, Version 2.0. // +using System; +using System.Buffers; + namespace ImageSharp.Formats { /// /// Represents a Huffman tree /// - internal class Huffman + internal struct Huffman : IDisposable { - /// - /// Initializes a new instance of the class. - /// - /// The log-2 size of the Huffman decoder's look-up table. - /// The maximum (inclusive) number of codes in a Huffman tree. - /// The maximum (inclusive) number of bits in a Huffman code. - public Huffman(int lutSize, int maxNCodes, int maxCodeLength) + private static ArrayPool UshortBuffer = + //ArrayPool.Shared; + ArrayPool.Create(1 << JpegDecoderCore.LutSize, 50); + + private static ArrayPool ByteBuffer = + //ArrayPool.Shared; + ArrayPool.Create(JpegDecoderCore.MaxNCodes, 50); + + private static readonly ArrayPool IntBuffer = + //ArrayPool.Shared; + ArrayPool.Create(JpegDecoderCore.MaxCodeLength, 50); + + public void Init(int lutSize, int maxNCodes, int maxCodeLength) { - this.Lut = new ushort[1 << lutSize]; - this.Values = new byte[maxNCodes]; - this.MinCodes = new int[maxCodeLength]; - this.MaxCodes = new int[maxCodeLength]; - this.Indices = new int[maxCodeLength]; - this.Length = 0; + //this.Lut = new ushort[1 << lutSize]; + //this.Values = new byte[maxNCodes]; + //this.MinCodes = new int[maxCodeLength]; + //this.MaxCodes = new int[maxCodeLength]; + //this.Indices = new int[maxCodeLength]; + + this.Lut = UshortBuffer.Rent(1 << lutSize); + this.Values = ByteBuffer.Rent(maxNCodes); + this.MinCodes = IntBuffer.Rent(maxCodeLength); + this.MaxCodes = IntBuffer.Rent(maxCodeLength); ; + this.Indices = IntBuffer.Rent(maxCodeLength); ; } /// /// Gets or sets the number of codes in the tree. /// - public int Length { get; set; } + public int Length; /// /// Gets the look-up table for the next LutSize bits in the bit-stream. @@ -37,28 +51,39 @@ namespace ImageSharp.Formats /// are 1 plus the code length, or 0 if the value is too large to fit in /// lutSize bits. /// - public ushort[] Lut { get; } + public ushort[] Lut; /// /// Gets the the decoded values, sorted by their encoding. /// - public byte[] Values { get; } + public byte[] Values; /// /// Gets the array of minimum codes. /// MinCodes[i] is the minimum code of length i, or -1 if there are no codes of that length. /// - public int[] MinCodes { get; } + public int[] MinCodes; /// /// Gets the array of maximum codes. /// MaxCodes[i] is the maximum code of length i, or -1 if there are no codes of that length. /// - public int[] MaxCodes { get; } + public int[] MaxCodes; /// /// Gets the array of indices. Indices[i] is the index into Values of MinCodes[i]. /// - public int[] Indices { get; } + public int[] Indices; + + public void Dispose() + { + UshortBuffer.Return(Lut, true); + ByteBuffer.Return(Values, true); + IntBuffer.Return(MinCodes, true); + IntBuffer.Return(MaxCodes, true); + IntBuffer.Return(Indices, true); + } } + + } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/IDCT.cs b/src/ImageSharp/Formats/Jpg/Components/IDCT.cs index bc145779a..88b493c4b 100644 --- a/src/ImageSharp/Formats/Jpg/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpg/Components/IDCT.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // +using System.Numerics; + namespace ImageSharp.Formats { /// @@ -39,7 +41,7 @@ namespace ImageSharp.Formats /// ASSP, Vol. ASSP- 32, pp. 803-816, Aug. 1984. /// /// The source block of coefficients - public static void Transform(Block src) + public static void Transform(ref Block src) { // Horizontal 1-D IDCT. for (int y = 0; y < 8; y++) @@ -165,5 +167,6 @@ namespace ImageSharp.Formats src[56 + x] = (y7 - y1) >> 14; } } + } } diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpg/JpegDecoder.cs index 666f1b35a..d3c64a9c0 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoder.cs @@ -83,8 +83,10 @@ namespace ImageSharp.Formats Guard.NotNull(image, "image"); Guard.NotNull(stream, "stream"); - JpegDecoderCore decoder = new JpegDecoderCore(); - decoder.Decode(image, stream, false); + using (JpegDecoderCore decoder = new JpegDecoderCore()) + { + decoder.Decode(image, stream, false); + } } /// diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index 332dcec48..4a7726be4 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // +using System.Runtime.CompilerServices; + namespace ImageSharp.Formats { using System; @@ -12,22 +14,22 @@ namespace ImageSharp.Formats /// /// Performs the jpeg decoding operation. /// - internal class JpegDecoderCore + internal class JpegDecoderCore : IDisposable { /// /// The maximum (inclusive) number of bits in a Huffman code. /// - private const int MaxCodeLength = 16; + internal const int MaxCodeLength = 16; /// /// The maximum (inclusive) number of codes in a Huffman tree. /// - private const int MaxNCodes = 256; + internal const int MaxNCodes = 256; /// /// The log-2 size of the Huffman decoder's look-up table. /// - private const int LutSize = 8; + internal const int LutSize = 8; /// /// The maximum number of color components @@ -44,6 +46,8 @@ namespace ImageSharp.Formats /// private const int MaxTh = 3; + private const int ThRowSize = MaxTh + 1; + /// /// The maximum number of quantization tables /// @@ -85,7 +89,9 @@ namespace ImageSharp.Formats /// /// The huffman trees /// - private readonly Huffman[,] huffmanTrees; + //private readonly Huffman[,] huffmanTrees; + + private readonly Huffman[] huffmanTrees; /// /// Quantization tables, in zigzag order. @@ -187,13 +193,17 @@ namespace ImageSharp.Formats /// private short verticalResolution; + private int blockIndex; + /// /// Initializes a new instance of the class. /// public JpegDecoderCore() { - this.huffmanTrees = new Huffman[MaxTc + 1, MaxTh + 1]; - this.quantizationTables = new Block[MaxTq + 1]; + //this.huffmanTrees = new Huffman[MaxTc + 1, MaxTh + 1]; + this.huffmanTrees = new Huffman[(MaxTc + 1)*(MaxTh + 1)]; + + this.quantizationTables = Block.CreateArray(MaxTq + 1); this.temp = new byte[2 * Block.BlockSize]; this.componentArray = new Component[MaxComponents]; this.progCoeffs = new Block[MaxComponents][]; @@ -201,25 +211,29 @@ namespace ImageSharp.Formats this.bytes = new Bytes(); // TODO: This looks like it could be static. + for (int i = 0; i < MaxTc + 1; i++) { for (int j = 0; j < MaxTh + 1; j++) { - this.huffmanTrees[i, j] = new Huffman(LutSize, MaxNCodes, MaxCodeLength); + //this.huffmanTrees[i, j].Init(LutSize, MaxNCodes, MaxCodeLength); + this.huffmanTrees[i* ThRowSize + j].Init(LutSize, MaxNCodes, MaxCodeLength); } } - for (int i = 0; i < this.quantizationTables.Length; i++) - { - this.quantizationTables[i] = new Block(); - } + //for (int i = 0; i < this.quantizationTables.Length; i++) + //{ + // //this.quantizationTables[i] = new Block(); + // this.quantizationTables[i].Init(); + //} - for (int i = 0; i < this.componentArray.Length; i++) - { - this.componentArray[i] = new Component(); - } + //for (int i = 0; i < this.componentArray.Length; i++) + //{ + // this.componentArray[i] = new Component(); + //} } + /// /// Decodes the image from the specified this._stream and sets /// the data to image. @@ -501,92 +515,96 @@ namespace ImageSharp.Formats throw new ImageFormatException("Bad Th value"); } - Huffman huffman = this.huffmanTrees[tc, th]; + ProcessDefineHuffmanTablesMarkerLoop(ref this.huffmanTrees[tc* ThRowSize + th], ref remaining); + } + } - // Read nCodes and huffman.Valuess (and derive h.Length). - // nCodes[i] is the number of codes with code length i. - // h.Length is the total number of codes. - huffman.Length = 0; + private void ProcessDefineHuffmanTablesMarkerLoop(ref Huffman huffman, ref int remaining) + { - int[] ncodes = new int[MaxCodeLength]; - for (int i = 0; i < ncodes.Length; i++) - { - ncodes[i] = this.temp[i + 1]; - huffman.Length += ncodes[i]; - } + // Read nCodes and huffman.Valuess (and derive h.Length). + // nCodes[i] is the number of codes with code length i. + // h.Length is the total number of codes. + huffman.Length = 0; - if (huffman.Length == 0) - { - throw new ImageFormatException("Huffman table has zero length"); - } + int[] ncodes = new int[MaxCodeLength]; + for (int i = 0; i < ncodes.Length; i++) + { + ncodes[i] = this.temp[i + 1]; + huffman.Length += ncodes[i]; + } - if (huffman.Length > MaxNCodes) - { - throw new ImageFormatException("Huffman table has excessive length"); - } + if (huffman.Length == 0) + { + throw new ImageFormatException("Huffman table has zero length"); + } - remaining -= huffman.Length + 17; - if (remaining < 0) - { - throw new ImageFormatException("DHT has wrong length"); - } + if (huffman.Length > MaxNCodes) + { + throw new ImageFormatException("Huffman table has excessive length"); + } + + remaining -= huffman.Length + 17; + if (remaining < 0) + { + throw new ImageFormatException("DHT has wrong length"); + } - this.ReadFull(huffman.Values, 0, huffman.Length); + this.ReadFull(huffman.Values, 0, huffman.Length); - // Derive the look-up table. - for (int i = 0; i < huffman.Lut.Length; i++) - { - huffman.Lut[i] = 0; - } + // Derive the look-up table. + for (int i = 0; i < huffman.Lut.Length; i++) + { + huffman.Lut[i] = 0; + } - uint x = 0, code = 0; + uint x = 0, code = 0; + + for (int i = 0; i < LutSize; i++) + { + code <<= 1; - for (int i = 0; i < LutSize; i++) + for (int j = 0; j < ncodes[i]; j++) { - code <<= 1; + // The codeLength is 1+i, so shift code by 8-(1+i) to + // calculate the high bits for every 8-bit sequence + // whose codeLength's high bits matches code. + // The high 8 bits of lutValue are the encoded value. + // The low 8 bits are 1 plus the codeLength. + byte base2 = (byte) (code << (7 - i)); + ushort lutValue = (ushort) ((huffman.Values[x] << 8) | (2 + i)); - for (int j = 0; j < ncodes[i]; j++) + for (int k = 0; k < 1 << (7 - i); k++) { - // The codeLength is 1+i, so shift code by 8-(1+i) to - // calculate the high bits for every 8-bit sequence - // whose codeLength's high bits matches code. - // The high 8 bits of lutValue are the encoded value. - // The low 8 bits are 1 plus the codeLength. - byte base2 = (byte)(code << (7 - i)); - ushort lutValue = (ushort)((huffman.Values[x] << 8) | (2 + i)); - - for (int k = 0; k < 1 << (7 - i); k++) - { - huffman.Lut[base2 | k] = lutValue; - } - - code++; - x++; + huffman.Lut[base2 | k] = lutValue; } + + code++; + x++; } + } - // Derive minCodes, maxCodes, and indices. - int c = 0, index = 0; - for (int i = 0; i < ncodes.Length; i++) + // Derive minCodes, maxCodes, and indices. + int c = 0, index = 0; + for (int i = 0; i < ncodes.Length; i++) + { + int nc = ncodes[i]; + if (nc == 0) { - int nc = ncodes[i]; - if (nc == 0) - { - huffman.MinCodes[i] = -1; - huffman.MaxCodes[i] = -1; - huffman.Indices[i] = -1; - } - else - { - huffman.MinCodes[i] = c; - huffman.MaxCodes[i] = c + nc - 1; - huffman.Indices[i] = index; - c += nc; - index += nc; - } - - c <<= 1; + huffman.MinCodes[i] = -1; + huffman.MaxCodes[i] = -1; + huffman.Indices[i] = -1; + } + else + { + huffman.MinCodes[i] = c; + huffman.MaxCodes[i] = c + nc - 1; + huffman.Indices[i] = index; + c += nc; + index += nc; } + + c <<= 1; } } @@ -595,7 +613,7 @@ namespace ImageSharp.Formats /// /// The huffman value /// The - private byte DecodeHuffman(Huffman huffman) + private byte DecodeHuffman(ref Huffman huffman) { if (huffman.Length == 0) { @@ -1385,7 +1403,7 @@ namespace ImageSharp.Formats byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; TColor packed = default(TColor); - this.PackYcbCr(ref packed, yy, cb, cr); + PackYcbCr(ref packed, yy, cb, cr); pixels[x, y] = packed; } }); @@ -1453,6 +1471,8 @@ namespace ImageSharp.Formats } } + private Block scanWorkerBlock = Block.Create(); + /// /// Processes the SOS (Start of scan marker). /// @@ -1479,7 +1499,8 @@ namespace ImageSharp.Formats this.ReadFull(this.temp, 0, remaining); byte scanComponentCount = this.temp[0]; - if (remaining != 4 + (2 * scanComponentCount)) + int scanComponentCountX2 = 2 * scanComponentCount; + if (remaining != 4 + scanComponentCountX2) { throw new ImageFormatException("SOS length inconsistent with number of components"); } @@ -1489,51 +1510,7 @@ namespace ImageSharp.Formats for (int i = 0; i < scanComponentCount; i++) { - // Component selector. - int cs = this.temp[1 + (2 * i)]; - int compIndex = -1; - for (int j = 0; j < this.componentCount; j++) - { - Component compv = this.componentArray[j]; - if (cs == compv.Identifier) - { - compIndex = j; - } - } - - if (compIndex < 0) - { - throw new ImageFormatException("Unknown component selector"); - } - - scan[i].Index = (byte)compIndex; - - // Section B.2.3 states that "the value of Cs_j shall be different from - // the values of Cs_1 through Cs_(j-1)". Since we have previously - // verified that a frame's component identifiers (C_i values in section - // B.2.2) are unique, it suffices to check that the implicit indexes - // into comp are unique. - for (int j = 0; j < i; j++) - { - if (scan[i].Index == scan[j].Index) - { - throw new ImageFormatException("Repeated component selector"); - } - } - - totalHv += this.componentArray[compIndex].HorizontalFactor * this.componentArray[compIndex].VerticalFactor; - - scan[i].DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); - if (scan[i].DcTableSelector > MaxTh) - { - throw new ImageFormatException("Bad DC table selector value"); - } - - scan[i].AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); - if (scan[i].AcTableSelector > MaxTh) - { - throw new ImageFormatException("Bad AC table selector value"); - } + ProcessScanImpl(i, ref scan[i], scan, ref totalHv); } // Section B.2.3 states that if there is more than one component then the @@ -1564,10 +1541,10 @@ namespace ImageSharp.Formats if (this.isProgressive) { - zigStart = this.temp[1 + (2 * scanComponentCount)]; - zigEnd = this.temp[2 + (2 * scanComponentCount)]; - ah = this.temp[3 + (2 * scanComponentCount)] >> 4; - al = this.temp[3 + (2 * scanComponentCount)] & 0x0f; + zigStart = this.temp[1 + scanComponentCountX2]; + zigEnd = this.temp[2 + scanComponentCountX2]; + ah = this.temp[3 + scanComponentCountX2] >> 4; + al = this.temp[3 + scanComponentCountX2] & 0x0f; if ((zigStart == 0 && zigEnd != 0) || zigStart > zigEnd || Block.BlockSize <= zigEnd) { @@ -1603,11 +1580,11 @@ namespace ImageSharp.Formats int compIndex = scan[i].Index; if (this.progCoeffs[compIndex] == null) { - this.progCoeffs[compIndex] = new Block[mxx * myy * this.componentArray[compIndex].HorizontalFactor * this.componentArray[compIndex].VerticalFactor]; + this.progCoeffs[compIndex] = Block.CreateArray(mxx * myy * this.componentArray[compIndex].HorizontalFactor * this.componentArray[compIndex].VerticalFactor); for (int j = 0; j < this.progCoeffs[compIndex].Length; j++) { - this.progCoeffs[compIndex][j] = new Block(); + this.progCoeffs[compIndex][j].Init(); } } } @@ -1619,7 +1596,7 @@ namespace ImageSharp.Formats byte expectedRst = JpegConstants.Markers.RST0; // b is the decoded coefficients block, in natural (not zig-zag) order. - Block b; + //Block b; int[] dc = new int[MaxComponents]; // bx and by are the location of the current block, in units of 8x8 @@ -1635,7 +1612,7 @@ namespace ImageSharp.Formats int compIndex = scan[i].Index; int hi = this.componentArray[compIndex].HorizontalFactor; int vi = this.componentArray[compIndex].VerticalFactor; - Block qt = this.quantizationTables[this.componentArray[compIndex].Selector]; + for (int j = 0; j < hi * vi; j++) { @@ -1678,168 +1655,27 @@ namespace ImageSharp.Formats } } - // Load the previous partially decoded coefficients, if applicable. - b = this.isProgressive ? this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] : new Block(); - - if (ah != 0) + var qtIndex = this.componentArray[compIndex].Selector; + + if (this.isProgressive) // Load the previous partially decoded coefficients, if applicable. { - this.Refine(b, this.huffmanTrees[AcTable, scan[i].AcTableSelector], zigStart, zigEnd, 1 << al); + blockIndex = ((@by * mxx) * hi) + bx; + ProcessBlockImpl(ah, + ref this.progCoeffs[compIndex][blockIndex], + scan, i, zigStart, zigEnd, al, dc, compIndex, @by, mxx, hi, bx, + ref this.quantizationTables[qtIndex] + ); } else { - int zig = zigStart; - if (zig == 0) - { - zig++; - - // Decode the DC coefficient, as specified in section F.2.2.1. - byte value = this.DecodeHuffman(this.huffmanTrees[DcTable, scan[i].DcTableSelector]); - if (value > 16) - { - throw new ImageFormatException("Excessive DC component"); - } - - int deltaDC = this.ReceiveExtend(value); - dc[compIndex] += deltaDC; - b[0] = dc[compIndex] << al; - } - - if (zig <= zigEnd && this.eobRun > 0) - { - this.eobRun--; - } - else - { - // Decode the AC coefficients, as specified in section F.2.2.2. - Huffman huffv = this.huffmanTrees[AcTable, scan[i].AcTableSelector]; - for (; zig <= zigEnd; zig++) - { - byte value = this.DecodeHuffman(huffv); - byte val0 = (byte)(value >> 4); - byte val1 = (byte)(value & 0x0f); - if (val1 != 0) - { - zig += val0; - if (zig > zigEnd) - { - break; - } - - int ac = this.ReceiveExtend(val1); - b[Unzig[zig]] = ac << al; - } - else - { - if (val0 != 0x0f) - { - this.eobRun = (ushort)(1 << val0); - if (val0 != 0) - { - this.eobRun |= (ushort)this.DecodeBits(val0); - } - - this.eobRun--; - break; - } - - zig += 0x0f; - } - } - } - } - - if (this.isProgressive) - { - if (zigEnd != Block.BlockSize - 1 || al != 0) - { - // We haven't completely decoded this 8x8 block. Save the coefficients. - this.progCoeffs[compIndex][((by * mxx) * hi) + bx] = b; - - // At this point, we could execute the rest of the loop body to dequantize and - // perform the inverse DCT, to save early stages of a progressive image to the - // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, - // the jpeg.Decode function does not return until the entire image is decoded, - // so we "continue" here to avoid wasted computation. - continue; - } - } - - // Dequantize, perform the inverse DCT and store the block to the image. - for (int zig = 0; zig < Block.BlockSize; zig++) - { - b[Unzig[zig]] *= qt[zig]; - } - - IDCT.Transform(b); - - byte[] dst; - int offset; - int stride; - - if (this.componentCount == 1) - { - dst = this.grayImage.Pixels; - stride = this.grayImage.Stride; - offset = this.grayImage.Offset + (8 * ((by * this.grayImage.Stride) + bx)); - } - else - { - switch (compIndex) - { - case 0: - dst = this.ycbcrImage.YChannel; - stride = this.ycbcrImage.YStride; - offset = this.ycbcrImage.YOffset + (8 * ((by * this.ycbcrImage.YStride) + bx)); - break; - - case 1: - dst = this.ycbcrImage.CbChannel; - stride = this.ycbcrImage.CStride; - offset = this.ycbcrImage.COffset + (8 * ((by * this.ycbcrImage.CStride) + bx)); - break; - - case 2: - dst = this.ycbcrImage.CrChannel; - stride = this.ycbcrImage.CStride; - offset = this.ycbcrImage.COffset + (8 * ((by * this.ycbcrImage.CStride) + bx)); - break; - - case 3: - - dst = this.blackPixels; - stride = this.blackStride; - offset = 8 * ((by * this.blackStride) + bx); - break; - - default: - throw new ImageFormatException("Too many components"); - } - } - - // Level shift by +128, clip to [0, 255], and write to dst. - for (int y = 0; y < 8; y++) - { - int y8 = y * 8; - int yStride = y * stride; - - for (int x = 0; x < 8; x++) - { - int c = b[y8 + x]; - if (c < -128) - { - c = 0; - } - else if (c > 127) - { - c = 255; - } - else - { - c += 128; - } - - dst[yStride + x + offset] = (byte)c; - } + //var b = Block.Create(); + scanWorkerBlock.Clear(); + + ProcessBlockImpl(ah, ref scanWorkerBlock, scan, i, zigStart, zigEnd, al, dc, compIndex, @by, mxx, hi, + bx, ref this.quantizationTables[qtIndex] + ); + + //b.Dispose(); } } @@ -1882,6 +1718,237 @@ namespace ImageSharp.Formats // for my } + private void ProcessBlockImpl(int ah, ref Block b, Scan[] scan, int i, int zigStart, int zigEnd, int al, + int[] dc, int compIndex, int @by, int mxx, int hi, int bx, ref Block qt) + { + if (ah != 0) + { + this.Refine(ref b, ref this.huffmanTrees[AcTable * ThRowSize + scan[i].AcTableSelector], zigStart, zigEnd, 1 << al); + } + else + { + int zig = zigStart; + if (zig == 0) + { + zig++; + + // Decode the DC coefficient, as specified in section F.2.2.1. + byte value = this.DecodeHuffman(ref this.huffmanTrees[DcTable * ThRowSize + scan[i].DcTableSelector]); + if (value > 16) + { + throw new ImageFormatException("Excessive DC component"); + } + + int deltaDC = this.ReceiveExtend(value); + dc[compIndex] += deltaDC; + b[0] = dc[compIndex] << al; + } + + if (zig <= zigEnd && this.eobRun > 0) + { + this.eobRun--; + } + else + { + // Decode the AC coefficients, as specified in section F.2.2.2. + //Huffman huffv = ; + for (; zig <= zigEnd; zig++) + { + byte value = this.DecodeHuffman(ref this.huffmanTrees[AcTable * ThRowSize + scan[i].AcTableSelector]); + byte val0 = (byte) (value >> 4); + byte val1 = (byte) (value & 0x0f); + if (val1 != 0) + { + zig += val0; + if (zig > zigEnd) + { + break; + } + + int ac = this.ReceiveExtend(val1); + b[Unzig[zig]] = ac << al; + } + else + { + if (val0 != 0x0f) + { + this.eobRun = (ushort) (1 << val0); + if (val0 != 0) + { + this.eobRun |= (ushort) this.DecodeBits(val0); + } + + this.eobRun--; + break; + } + + zig += 0x0f; + } + } + } + } + + if (this.isProgressive) + { + if (zigEnd != Block.BlockSize - 1 || al != 0) + { + // We haven't completely decoded this 8x8 block. Save the coefficients. + + this.progCoeffs[compIndex][((@by*mxx)*hi) + bx] = b.Clone(); + + // At this point, we could execute the rest of the loop body to dequantize and + // perform the inverse DCT, to save early stages of a progressive image to the + // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, + // the jpeg.Decode function does not return until the entire image is decoded, + // so we "continue" here to avoid wasted computation. + return; + } + } + + // Dequantize, perform the inverse DCT and store the block to the image. + for (int zig = 0; zig < Block.BlockSize; zig++) + { + b[Unzig[zig]] *= qt[zig]; + } + + IDCT.Transform(ref b); + + // ******* Other experimental variants: ************* + + // FluxJpeg: + // https://github.com/antonfirsov/ImageSharp/blob/master/src/ImageSharp46/Formats/Jpg/Components/FloatIDCT.cs + // FloatIDCT.Transform(ref b); + + // SIMD-based: + // https://github.com/antonfirsov/ImageSharp/blob/master/src/ImageSharp46/Formats/Jpg/Components/MagicDCT.cs + // MagicDCT.IDCT(ref b); + + byte[] dst; + int offset; + int stride; + + if (this.componentCount == 1) + { + dst = this.grayImage.Pixels; + stride = this.grayImage.Stride; + offset = this.grayImage.Offset + (8*((@by*this.grayImage.Stride) + bx)); + } + else + { + switch (compIndex) + { + case 0: + dst = this.ycbcrImage.YChannel; + stride = this.ycbcrImage.YStride; + offset = this.ycbcrImage.YOffset + (8*((@by*this.ycbcrImage.YStride) + bx)); + break; + + case 1: + dst = this.ycbcrImage.CbChannel; + stride = this.ycbcrImage.CStride; + offset = this.ycbcrImage.COffset + (8*((@by*this.ycbcrImage.CStride) + bx)); + break; + + case 2: + dst = this.ycbcrImage.CrChannel; + stride = this.ycbcrImage.CStride; + offset = this.ycbcrImage.COffset + (8*((@by*this.ycbcrImage.CStride) + bx)); + break; + + case 3: + + dst = this.blackPixels; + stride = this.blackStride; + offset = 8*((@by*this.blackStride) + bx); + break; + + default: + throw new ImageFormatException("Too many components"); + } + } + + // Level shift by +128, clip to [0, 255], and write to dst. + for (int y = 0; y < 8; y++) + { + int y8 = y*8; + int yStride = y*stride; + + for (int x = 0; x < 8; x++) + { + int c = b[y8 + x]; + if (c < -128) + { + c = 0; + } + else if (c > 127) + { + c = 255; + } + else + { + c += 128; + } + + dst[yStride + x + offset] = (byte) c; + } + } + } + + private void ProcessScanImpl(int i, ref Scan currentScan, Scan[] scan, ref int totalHv) + { + // Component selector. + int cs = this.temp[1 + (2 * i)]; + int compIndex = -1; + for (int j = 0; j < this.componentCount; j++) + { + //Component compv = ; + if (cs == this.componentArray[j].Identifier) + { + compIndex = j; + } + } + + if (compIndex < 0) + { + throw new ImageFormatException("Unknown component selector"); + } + + currentScan.Index = (byte)compIndex; + + ProcessComponentImpl(i, ref currentScan, scan, ref totalHv, ref this.componentArray[compIndex]); + } + + private void ProcessComponentImpl(int i, ref Scan currentScan, Scan[] scan, ref int totalHv, ref Component currentComponent) + { + // Section B.2.3 states that "the value of Cs_j shall be different from + // the values of Cs_1 through Cs_(j-1)". Since we have previously + // verified that a frame's component identifiers (C_i values in section + // B.2.2) are unique, it suffices to check that the implicit indexes + // into comp are unique. + for (int j = 0; j < i; j++) + { + if (currentScan.Index == scan[j].Index) + { + throw new ImageFormatException("Repeated component selector"); + } + } + + + totalHv += currentComponent.HorizontalFactor*currentComponent.VerticalFactor; + + currentScan.DcTableSelector = (byte) (this.temp[2 + (2*i)] >> 4); + if (currentScan.DcTableSelector > MaxTh) + { + throw new ImageFormatException("Bad DC table selector value"); + } + + currentScan.AcTableSelector = (byte) (this.temp[2 + (2*i)] & 0x0f); + if (currentScan.AcTableSelector > MaxTh) + { + throw new ImageFormatException("Bad AC table selector value"); + } + } + /// /// Decodes a successive approximation refinement block, as specified in section G.1.2. /// @@ -1890,7 +1957,7 @@ namespace ImageSharp.Formats /// The zig-zag start index /// The zig-zag end index /// The low transform offset - private void Refine(Block b, Huffman h, int zigStart, int zigEnd, int delta) + private void Refine(ref Block b, ref Huffman h, int zigStart, int zigEnd, int delta) { // Refining a DC component is trivial. if (zigStart == 0) @@ -1917,7 +1984,7 @@ namespace ImageSharp.Formats { bool done = false; int z = 0; - byte val = this.DecodeHuffman(h); + byte val = this.DecodeHuffman(ref h); int val0 = val >> 4; int val1 = val & 0x0f; @@ -2107,7 +2174,8 @@ namespace ImageSharp.Formats /// The y luminance component. /// The cb chroma component. /// The cr chroma component. - private void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) where TColor : struct, IPackedPixel where TPacked : struct { @@ -2199,5 +2267,24 @@ namespace ImageSharp.Formats private class EOFException : Exception { } + + public void Dispose() + { + scanWorkerBlock.Dispose(); + Block.DisposeAll(this.quantizationTables); + + foreach (Block[] blocks in progCoeffs) + { + if (blocks != null) + { + Block.DisposeAll(blocks); + } + } + + for (int i = 0; i < huffmanTrees.Length; i++) + { + huffmanTrees[i].Dispose(); + } + } } } diff --git a/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs index 69161eb02..75e00bc76 100644 --- a/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs @@ -246,6 +246,7 @@ namespace ImageSharp.Formats /// /// The AC luminance huffman table index /// + LuminanceAC = 1, // ReSharper restore UnusedMember.Local @@ -487,9 +488,9 @@ namespace ImageSharp.Formats /// The quantization table index. /// The previous DC value. /// The - private int WriteBlock(Block block, QuantIndex index, int prevDC) + private int WriteBlock(ref Block block, QuantIndex index, int prevDC) { - FDCT.Transform(block); + FDCT.Transform(ref block); // Emit the DC delta. int dc = Round(block[0], 8 * this.quant[(int)index][0]); @@ -540,7 +541,8 @@ namespace ImageSharp.Formats /// The red chroma block. /// The blue chroma block. // ReSharper disable StyleCop.SA1305 - private void ToYCbCr(PixelAccessor pixels, int x, int y, Block yBlock, Block cbBlock, Block crBlock) + private void ToYCbCr(PixelAccessor pixels, int x, int y, + ref Block yBlock, ref Block cbBlock, ref Block crBlock) // ReSharper restore StyleCop.SA1305 where TColor : struct, IPackedPixel where TPacked : struct @@ -577,7 +579,7 @@ namespace ImageSharp.Formats /// /// The destination block array /// The source block array. - private void Scale16X16To8X8(Block destination, Block[] source) + private void Scale16X16To8X8(ref Block destination, Block[] source) { for (int i = 0; i < 4; i++) { @@ -847,10 +849,9 @@ namespace ImageSharp.Formats where TColor : struct, IPackedPixel where TPacked : struct { - Block b = new Block(); - Block cb = new Block(); - Block cr = new Block(); - + Block b = Block.Create(); + Block cb = Block.Create(); + Block cr = Block.Create(); // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; @@ -858,12 +859,15 @@ namespace ImageSharp.Formats { for (int x = 0; x < pixels.Width; x += 8) { - this.ToYCbCr(pixels, x, y, b, cb, cr); - prevDCY = this.WriteBlock(b, QuantIndex.Luminance, prevDCY); - prevDCCb = this.WriteBlock(cb, QuantIndex.Chrominance, prevDCCb); - prevDCCr = this.WriteBlock(cr, QuantIndex.Chrominance, prevDCCr); + this.ToYCbCr(pixels, x, y, ref b, ref cb, ref cr); + prevDCY = this.WriteBlock(ref b, QuantIndex.Luminance, prevDCY); + prevDCCb = this.WriteBlock(ref cb, QuantIndex.Chrominance, prevDCCb); + prevDCCr = this.WriteBlock(ref cr, QuantIndex.Chrominance, prevDCCr); } } + b.Dispose(); + cb.Dispose(); + cr.Dispose(); } /// @@ -877,23 +881,12 @@ namespace ImageSharp.Formats where TColor : struct, IPackedPixel where TPacked : struct { - Block b = new Block(); - Block[] cb = new Block[4]; - Block[] cr = new Block[4]; - + Block b = Block.Create(); + Block[] cb = Block.CreateArray(4); + Block[] cr = Block.CreateArray(4); // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; - - for (int i = 0; i < 4; i++) - { - cb[i] = new Block(); - } - - for (int i = 0; i < 4; i++) - { - cr[i] = new Block(); - } - + for (int y = 0; y < pixels.Height; y += 16) { for (int x = 0; x < pixels.Width; x += 16) @@ -903,16 +896,20 @@ namespace ImageSharp.Formats int xOff = (i & 1) * 8; int yOff = (i & 2) * 4; - this.ToYCbCr(pixels, x + xOff, y + yOff, b, cb[i], cr[i]); - prevDCY = this.WriteBlock(b, QuantIndex.Luminance, prevDCY); + this.ToYCbCr(pixels, x + xOff, y + yOff, ref b, ref cb[i], ref cr[i]); + prevDCY = this.WriteBlock(ref b, QuantIndex.Luminance, prevDCY); } - this.Scale16X16To8X8(b, cb); - prevDCCb = this.WriteBlock(b, QuantIndex.Chrominance, prevDCCb); - this.Scale16X16To8X8(b, cr); - prevDCCr = this.WriteBlock(b, QuantIndex.Chrominance, prevDCCr); + this.Scale16X16To8X8(ref b, cb); + prevDCCb = this.WriteBlock(ref b, QuantIndex.Chrominance, prevDCCb); + this.Scale16X16To8X8(ref b, cr); + prevDCCr = this.WriteBlock(ref b, QuantIndex.Chrominance, prevDCCr); } } + + b.Dispose(); + Block.DisposeAll(cb); + Block.DisposeAll(cr); } /// diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs index 549ac05ef..c91b0ad1b 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // +using ImageSharp.Formats; + namespace ImageSharp.Tests { using System.IO; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs new file mode 100644 index 000000000..57bce1504 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ImageSharp.Formats; +using Xunit; +using Xunit.Abstractions; + +namespace ImageSharp.Tests.Formats.Jpg +{ + public class JpegTests + { + + public const string TestOutputDirectory = "TestOutput/Jpeg"; + + private ITestOutputHelper Output { get; } + + public JpegTests(ITestOutputHelper output) + { + Output = output; + } + + protected string CreateTestOutputFile(string fileName) + { + if (!Directory.Exists(TestOutputDirectory)) + { + Directory.CreateDirectory(TestOutputDirectory); + } + + //string id = Guid.NewGuid().ToString().Substring(0, 4); + + string ext = Path.GetExtension(fileName); + fileName = Path.GetFileNameWithoutExtension(fileName); + + return $"{TestOutputDirectory}/{fileName}{ext}"; + } + + protected Stream CreateOutputStream(string fileName) + { + fileName = CreateTestOutputFile(fileName); + Output?.WriteLine("Opened for write: "+fileName); + return File.OpenWrite(fileName); + } + + public static IEnumerable AllJpegFiles + => TestImages.Jpeg.All.Select(fn => new object[] {fn}); + + [Theory] + [MemberData(nameof(AllJpegFiles))] + public void OpenJpeg_SaveBmp(string jpegPath) + { + string bmpFileName = Path.GetFileNameWithoutExtension(jpegPath) + ".bmp"; + + using (var inputStream = File.OpenRead(jpegPath)) + { + var image = new Image(inputStream); + + using (var outputStream = CreateOutputStream(bmpFileName)) + { + image.Save(outputStream, new BmpFormat()); + } + } + } + + public static IEnumerable AllBmpFiles + => TestImages.Jpeg.All.Select(fn => new object[] {fn}); + + [Theory] + [MemberData(nameof(AllBmpFiles))] + public void OpenBmp_SaveJpeg(string bmpPath) + { + string jpegPath = Path.GetFileNameWithoutExtension(bmpPath) + ".jpeg"; + + using (var inputStream = File.OpenRead(bmpPath)) + { + var image = new Image(inputStream); + + using (var outputStream = CreateOutputStream(jpegPath)) + { + image.Save(outputStream, new JpegFormat()); + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTests.cs index c442b71c9..468ed6e13 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTests.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // +using ImageSharp.Formats; + namespace ImageSharp.Tests { using System.IO; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e6d3cd757..5b6ba654e 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -25,13 +25,20 @@ namespace ImageSharp.Tests { private static readonly string folder = "TestImages/Formats/Jpg/"; public static string Cmyk => folder + "cmyk.jpg"; - public static string Exif => folder + "exif.jpeg"; + public static string Exif => folder + "exif.jpg"; public static string Floorplan => folder + "Floorplan.jpeg"; public static string Calliphora => folder + "Calliphora.jpg"; public static string Turtle => folder + "turtle.jpg"; public static string Fb => folder + "fb.jpg"; public static string Progress => folder + "progress.jpg"; public static string GammaDalaiLamaGray => folder + "gamma_dalai_lama_gray.jpg"; + + public static string Geneserath => folder + "geneserath.jpg"; + + public static readonly string[] All = new[] + { + Cmyk, Exif, Floorplan, Calliphora, Turtle, Fb, Progress, GammaDalaiLamaGray, Geneserath + }; } public static class Bmp diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/geneserath.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/geneserath.jpg new file mode 100644 index 000000000..af0c04a1c --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/geneserath.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c71e5b1ba181a5b17ba9814c2025650de592efabf4062bd77baa3c8e774df007 +size 223841