diff --git a/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs b/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs index 13475af09e..2b8c15ab3c 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs @@ -6,6 +6,7 @@ namespace ImageSharp.Formats.Jpg { using System; + using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bits.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bits.cs index 88aa8a3fe8..02f585be02 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bits.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bits.cs @@ -17,13 +17,13 @@ namespace ImageSharp.Formats.Jpg /// /// Gets or sets the accumulator. /// - public uint Accumulator; + public int Accumulator; /// /// Gets or sets the mask. /// 0, with mask==0 when unreadbits==0.]]> /// - public uint Mask; + public int Mask; /// /// Gets or sets the number of unread bits in the accumulator. @@ -36,72 +36,124 @@ namespace ImageSharp.Formats.Jpg /// the caller is the one responsible for first checking that bits.UnreadBits < n. /// /// The number of bits to ensure. - /// Jpeg decoder - /// Error code + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal JpegDecoderCore.ErrorCodes EnsureNBits(int n, JpegDecoderCore decoder) + public void EnsureNBits(int n, ref InputProcessor inputProcessor) + { + DecoderErrorCode errorCode = this.EnsureNBitsUnsafe(n, ref inputProcessor); + errorCode.EnsureNoError(); + } + + /// + /// Reads bytes from the byte buffer to ensure that bits.UnreadBits is at + /// least n. For best performance (avoiding function calls inside hot loops), + /// the caller is the one responsible for first checking that bits.UnreadBits < n. + /// This method does not throw. Returns instead. + /// + /// The number of bits to ensure. + /// The + /// Error code + public DecoderErrorCode EnsureNBitsUnsafe(int n, ref InputProcessor inputProcessor) { while (true) { - JpegDecoderCore.ErrorCodes errorCode; - - // Grab the decode bytes, use them and then set them - // back on the decoder. - Bytes decoderBytes = decoder.Bytes; - byte c = decoderBytes.ReadByteStuffedByte(decoder.InputStream, out errorCode); - decoder.Bytes = decoderBytes; - - if (errorCode != JpegDecoderCore.ErrorCodes.NoError) + DecoderErrorCode errorCode = this.EnsureBitsStepImpl(ref inputProcessor); + if (errorCode != DecoderErrorCode.NoError || this.UnreadBits >= n) { return errorCode; } + } + } - this.Accumulator = (this.Accumulator << 8) | c; - this.UnreadBits += 8; - if (this.Mask == 0) - { - this.Mask = 1 << 7; - } - else - { - this.Mask <<= 8; - } + /// + /// Unrolled version of for n==8 + /// + /// The + /// A + public DecoderErrorCode Ensure8BitsUnsafe(ref InputProcessor inputProcessor) + { + return this.EnsureBitsStepImpl(ref inputProcessor); + } - if (this.UnreadBits >= n) - { - return JpegDecoderCore.ErrorCodes.NoError; - } - } + /// + /// Unrolled version of for n==1 + /// + /// The + /// A + public DecoderErrorCode Ensure1BitUnsafe(ref InputProcessor inputProcessor) + { + return this.EnsureBitsStepImpl(ref inputProcessor); } /// /// Receive extend /// /// Byte - /// Jpeg decoder + /// The /// Read bits value - internal int ReceiveExtend(byte t, JpegDecoderCore decoder) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReceiveExtend(int t, ref InputProcessor inputProcessor) + { + int x; + DecoderErrorCode errorCode = this.ReceiveExtendUnsafe(t, ref inputProcessor, out x); + errorCode.EnsureNoError(); + return x; + } + + /// + /// Receive extend + /// + /// Byte + /// The + /// Read bits value + /// The + public DecoderErrorCode ReceiveExtendUnsafe(int t, ref InputProcessor inputProcessor, out int x) { if (this.UnreadBits < t) { - JpegDecoderCore.ErrorCodes errorCode = this.EnsureNBits(t, decoder); - if (errorCode != JpegDecoderCore.ErrorCodes.NoError) + DecoderErrorCode errorCode = this.EnsureNBitsUnsafe(t, ref inputProcessor); + if (errorCode != DecoderErrorCode.NoError) { - throw new JpegDecoderCore.MissingFF00Exception(); + x = int.MaxValue; + return errorCode; } } this.UnreadBits -= t; this.Mask >>= t; int s = 1 << t; - int x = (int)((this.Accumulator >> this.UnreadBits) & (s - 1)); + x = (int)((this.Accumulator >> this.UnreadBits) & (s - 1)); if (x < (s >> 1)) { x += ((-1) << t) + 1; } - return x; + return DecoderErrorCode.NoError; + } + + private DecoderErrorCode EnsureBitsStepImpl(ref InputProcessor inputProcessor) + { + int c; + DecoderErrorCode errorCode = inputProcessor.Bytes.ReadByteStuffedByteUnsafe(inputProcessor.InputStream, out c); + + if (errorCode != DecoderErrorCode.NoError) + { + return errorCode; + } + + this.Accumulator = (this.Accumulator << 8) | c; + this.UnreadBits += 8; + if (this.Mask == 0) + { + this.Mask = 1 << 7; + } + else + { + this.Mask <<= 8; + } + + return errorCode; } } } \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bytes.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bytes.cs index b91420b42e..0e57e98d89 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bytes.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/Bytes.cs @@ -16,6 +16,11 @@ namespace ImageSharp.Formats.Jpg /// internal struct Bytes : IDisposable { + /// + /// Specifies the buffer size for and + /// + public const int BufferSize = 4096; + /// /// Gets or sets the buffer. /// buffer[i:j] are the buffered bytes read from the underlying @@ -23,6 +28,11 @@ namespace ImageSharp.Formats.Jpg /// public byte[] Buffer; + /// + /// Values of converted to -s + /// + public int[] BufferAsInt; + /// /// Start of bytes read /// @@ -39,7 +49,9 @@ namespace ImageSharp.Formats.Jpg /// public int UnreadableBytes; - private static readonly ArrayPool ArrayPool = ArrayPool.Create(4096, 50); + private static readonly ArrayPool BytePool = ArrayPool.Create(BufferSize, 50); + + private static readonly ArrayPool IntPool = ArrayPool.Create(BufferSize, 50); /// /// Creates a new instance of the , and initializes it's buffer. @@ -47,7 +59,11 @@ namespace ImageSharp.Formats.Jpg /// The bytes created public static Bytes Create() { - return new Bytes { Buffer = ArrayPool.Rent(4096) }; + return new Bytes + { + Buffer = BytePool.Rent(BufferSize), + BufferAsInt = IntPool.Rent(BufferSize) + }; } /// @@ -57,68 +73,72 @@ namespace ImageSharp.Formats.Jpg { if (this.Buffer != null) { - ArrayPool.Return(this.Buffer); + BytePool.Return(this.Buffer); + IntPool.Return(this.BufferAsInt); } this.Buffer = null; + this.BufferAsInt = null; } /// /// ReadByteStuffedByte is like ReadByte but is for byte-stuffed Huffman data. /// /// Input stream - /// Error code - /// The - internal byte ReadByteStuffedByte(Stream inputStream, out JpegDecoderCore.ErrorCodes errorCode) + /// The result byte as + /// The + public DecoderErrorCode ReadByteStuffedByteUnsafe(Stream inputStream, out int x) { - byte x; - - errorCode = JpegDecoderCore.ErrorCodes.NoError; - // Take the fast path if bytes.buf contains at least two bytes. if (this.I + 2 <= this.J) { - x = this.Buffer[this.I]; + x = this.BufferAsInt[this.I]; this.I++; this.UnreadableBytes = 1; - if (x != JpegConstants.Markers.XFF) + if (x != JpegConstants.Markers.XFFInt) { - return x; + return DecoderErrorCode.NoError; } - if (this.Buffer[this.I] != 0x00) + if (this.BufferAsInt[this.I] != 0x00) { - errorCode = JpegDecoderCore.ErrorCodes.MissingFF00; - return 0; - - // throw new MissingFF00Exception(); + return DecoderErrorCode.MissingFF00; } this.I++; this.UnreadableBytes = 2; - return JpegConstants.Markers.XFF; + x = JpegConstants.Markers.XFF; + return DecoderErrorCode.NoError; } this.UnreadableBytes = 0; - x = this.ReadByte(inputStream); + DecoderErrorCode errorCode = this.ReadByteAsIntUnsafe(inputStream, out x); this.UnreadableBytes = 1; + if (errorCode != DecoderErrorCode.NoError) + { + return errorCode; + } + if (x != JpegConstants.Markers.XFF) { - return x; + return DecoderErrorCode.NoError; } - x = this.ReadByte(inputStream); + errorCode = this.ReadByteAsIntUnsafe(inputStream, out x); this.UnreadableBytes = 2; - if (x != 0x00) + if (errorCode != DecoderErrorCode.NoError) { - errorCode = JpegDecoderCore.ErrorCodes.MissingFF00; - return 0; + return errorCode; + } - // throw new MissingFF00Exception(); + if (x != 0x00) + { + return DecoderErrorCode.MissingFF00; } - return JpegConstants.Markers.XFF; + x = JpegConstants.Markers.XFF; + return DecoderErrorCode.NoError; } /// @@ -127,30 +147,92 @@ namespace ImageSharp.Formats.Jpg /// Input stream /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal byte ReadByte(Stream inputStream) + public byte ReadByte(Stream inputStream) + { + byte result; + DecoderErrorCode errorCode = this.ReadByteUnsafe(inputStream, out result); + errorCode.EnsureNoError(); + return result; + } + + /// + /// Extracts the next byte, whether buffered or not buffered into the result out parameter. It does not care about byte stuffing. + /// This method does not throw on format error, it returns a instead. + /// + /// Input stream + /// The result as out parameter + /// The + public DecoderErrorCode ReadByteUnsafe(Stream inputStream, out byte result) + { + DecoderErrorCode errorCode = DecoderErrorCode.NoError; + while (this.I == this.J) + { + errorCode = this.FillUnsafe(inputStream); + if (errorCode != DecoderErrorCode.NoError) + { + result = 0; + return errorCode; + } + } + + result = this.Buffer[this.I]; + this.I++; + this.UnreadableBytes = 0; + return errorCode; + } + + /// + /// Same as but the result is an + /// + /// The input stream + /// The result + /// A + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DecoderErrorCode ReadByteAsIntUnsafe(Stream inputStream, out int result) { + DecoderErrorCode errorCode = DecoderErrorCode.NoError; while (this.I == this.J) { - this.Fill(inputStream); + errorCode = this.FillUnsafe(inputStream); + if (errorCode != DecoderErrorCode.NoError) + { + result = 0; + return errorCode; + } } - byte x = this.Buffer[this.I]; + result = this.BufferAsInt[this.I]; this.I++; this.UnreadableBytes = 0; - return x; + return errorCode; } /// /// Fills up the bytes buffer from the underlying stream. /// It should only be called when there are no unread bytes in bytes. /// + /// Thrown when reached end of stream unexpectedly. /// Input stream [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Fill(Stream inputStream) + public void Fill(Stream inputStream) + { + DecoderErrorCode errorCode = this.FillUnsafe(inputStream); + errorCode.EnsureNoError(); + } + + /// + /// Fills up the bytes buffer from the underlying stream. + /// It should only be called when there are no unread bytes in bytes. + /// This method does not throw , returns a instead! + /// + /// Input stream + /// The + public DecoderErrorCode FillUnsafe(Stream inputStream) { if (this.I != this.J) { - throw new ImageFormatException("Fill called when unread bytes exist."); + // Unrecoverable error in the input, throwing! + DecoderThrowHelper.ThrowImageFormatException.FillCalledWhenUnreadBytesExist(); } // Move the last 2 bytes to the start of the buffer, in case we need @@ -167,10 +249,17 @@ namespace ImageSharp.Formats.Jpg int n = inputStream.Read(this.Buffer, this.J, this.Buffer.Length - this.J); if (n == 0) { - throw new JpegDecoderCore.EOFException(); + return DecoderErrorCode.UnexpectedEndOfStream; } this.J += n; + + for (int i = 0; i < this.Buffer.Length; i++) + { + this.BufferAsInt[i] = this.Buffer[i]; + } + + return DecoderErrorCode.NoError; } } } \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecodedBlockMemento.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecodedBlockMemento.cs new file mode 100644 index 0000000000..04ece04ee8 --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecodedBlockMemento.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpg +{ + using System; + using System.Buffers; + + /// + /// A structure to store unprocessed instances and their coordinates while scanning the image. + /// + internal struct DecodedBlockMemento + { + /// + /// A value indicating whether the instance is initialized. + /// + public bool Initialized; + + /// + /// X coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0)) + /// + public int Bx; + + /// + /// Y coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0)) + /// + public int By; + + /// + /// The + /// + public Block8x8F Block; + + /// + /// Store the block data into a at the given index of an . + /// + /// The array + /// The index in the array + /// X coordinate of the block + /// Y coordinate of the block + /// The + public static void Store(ref DecodedBlockMemento.Array blockArray, int index, int bx, int by, ref Block8x8F block) + { + if (index >= blockArray.Count) + { + throw new IndexOutOfRangeException("Block index is out of range in DecodedBlockMemento.Store()!"); + } + + blockArray.Buffer[index].Initialized = true; + blockArray.Buffer[index].Bx = bx; + blockArray.Buffer[index].By = by; + blockArray.Buffer[index].Block = block; + } + + /// + /// Because has no information for rented arrays, we need to store the count and the buffer separately. + /// + public struct Array : IDisposable + { + /// + /// The used to pool data in . + /// Should always clean arrays when returning! + /// + private static readonly ArrayPool ArrayPool = ArrayPool.Create(); + + /// + /// Initializes a new instance of the struct. Rents a buffer. + /// + /// The number of valid -s + public Array(int count) + { + this.Count = count; + this.Buffer = ArrayPool.Rent(count); + } + + /// + /// Gets the number of actual -s inside + /// + public int Count { get; } + + /// + /// Gets the rented buffer. + /// + public DecodedBlockMemento[] Buffer { get; private set; } + + /// + /// Returns the rented buffer to the pool. + /// + public void Dispose() + { + if (this.Buffer != null) + { + ArrayPool.Return(this.Buffer, true); + this.Buffer = null; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecoderErrorCode.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecoderErrorCode.cs new file mode 100644 index 0000000000..8b82184faf --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecoderErrorCode.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Represents "recoverable" decoder errors. + /// + internal enum DecoderErrorCode + { + /// + /// NoError + /// + NoError, + + /// + /// MissingFF00 + /// + // ReSharper disable once InconsistentNaming + MissingFF00, + + /// + /// End of stream reached unexpectedly + /// + UnexpectedEndOfStream + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecoderThrowHelper.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecoderThrowHelper.cs new file mode 100644 index 0000000000..9ce5ea4146 --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/DecoderThrowHelper.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpg +{ + using System; + using System.Runtime.CompilerServices; + + /// + /// Encapsulates exception thrower methods for the Jpeg Encoder + /// + internal static class DecoderThrowHelper + { + /// + /// Throws an exception that belongs to the given + /// + /// The + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowExceptionForErrorCode(this DecoderErrorCode errorCode) + { + switch (errorCode) + { + case DecoderErrorCode.NoError: + throw new ArgumentException("ThrowExceptionForErrorCode() called with NoError!", nameof(errorCode)); + case DecoderErrorCode.MissingFF00: + throw new MissingFF00Exception(); + case DecoderErrorCode.UnexpectedEndOfStream: + throw new EOFException(); + default: + throw new ArgumentOutOfRangeException(nameof(errorCode), errorCode, null); + } + } + + /// + /// Throws an exception if the given defines an error. + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EnsureNoError(this DecoderErrorCode errorCode) + { + if (errorCode != DecoderErrorCode.NoError) + { + ThrowExceptionForErrorCode(errorCode); + } + } + + /// + /// Throws an exception if the given is . + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EnsureNoEOF(this DecoderErrorCode errorCode) + { + if (errorCode == DecoderErrorCode.UnexpectedEndOfStream) + { + errorCode.ThrowExceptionForErrorCode(); + } + } + + /// + /// Encapsulates methods throwing different flavours of -s. + /// + public static class ThrowImageFormatException + { + /// + /// Throws "Fill called when unread bytes exist". + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static void FillCalledWhenUnreadBytesExist() + { + throw new ImageFormatException("Fill called when unread bytes exist!"); + } + + /// + /// Throws "Bad Huffman code". + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static void BadHuffmanCode() + { + throw new ImageFormatException("Bad Huffman code!"); + } + + /// + /// Throws "Uninitialized Huffman table". + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static void UninitializedHuffmanTable() + { + throw new ImageFormatException("Uninitialized Huffman table"); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/EOFException.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/EOFException.cs new file mode 100644 index 0000000000..5ed25ef049 --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/EOFException.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpg +{ + using System; + + /// + /// The EOF (End of File exception). + /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker + /// TODO: Rename to UnexpectedEndOfStreamException + /// + internal class EOFException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public EOFException() + : base("Reached end of stream before proceeding EOI marker!") + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/HuffmanTree.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/HuffmanTree.cs index e06d644a7f..03013219c3 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Decoder/HuffmanTree.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/HuffmanTree.cs @@ -45,7 +45,7 @@ namespace ImageSharp.Formats.Jpg /// /// The log-2 size of the Huffman decoder's look-up table. /// - public const int LutSize = 8; + public const int LutSizeLog2 = 8; /// /// Gets or sets the number of codes in the tree. @@ -58,12 +58,12 @@ namespace ImageSharp.Formats.Jpg /// are 1 plus the code length, or 0 if the value is too large to fit in /// lutSize bits. /// - public ushort[] Lut; + public int[] Lut; /// /// Gets the the decoded values, sorted by their encoding. /// - public byte[] Values; + public int[] Values; /// /// Gets the array of minimum codes. @@ -82,11 +82,11 @@ namespace ImageSharp.Formats.Jpg /// public int[] Indices; - private static readonly ArrayPool UshortBuffer = ArrayPool.Create(1 << LutSize, 50); + private static readonly ArrayPool IntPool256 = ArrayPool.Create(MaxNCodes, 50); - private static readonly ArrayPool ByteBuffer = ArrayPool.Create(MaxNCodes, 50); + private static readonly ArrayPool BytePool256 = ArrayPool.Create(MaxNCodes, 50); - private static readonly ArrayPool IntBuffer = ArrayPool.Create(MaxCodeLength, 50); + private static readonly ArrayPool CodesPool16 = ArrayPool.Create(MaxCodeLength, 50); /// /// Creates and initializes an array of instances of size @@ -111,21 +111,21 @@ namespace ImageSharp.Formats.Jpg /// public void Dispose() { - UshortBuffer.Return(this.Lut, true); - ByteBuffer.Return(this.Values, true); - IntBuffer.Return(this.MinCodes, true); - IntBuffer.Return(this.MaxCodes, true); - IntBuffer.Return(this.Indices, true); + IntPool256.Return(this.Lut, true); + IntPool256.Return(this.Values, true); + CodesPool16.Return(this.MinCodes, true); + CodesPool16.Return(this.MaxCodes, true); + CodesPool16.Return(this.Indices, true); } /// /// Internal part of the DHT processor, whatever does it mean /// - /// The decoder instance + /// The decoder instance /// The temporal buffer that holds the data that has been read from the Jpeg stream /// Remaining bits public void ProcessDefineHuffmanTablesMarkerLoop( - JpegDecoderCore decoder, + ref InputProcessor inputProcessor, byte[] defineHuffmanTablesData, ref int remaining) { @@ -157,7 +157,21 @@ namespace ImageSharp.Formats.Jpg throw new ImageFormatException("DHT has wrong length"); } - decoder.ReadFull(this.Values, 0, this.Length); + byte[] values = null; + try + { + values = BytePool256.Rent(MaxNCodes); + inputProcessor.ReadFull(values, 0, this.Length); + + for (int i = 0; i < values.Length; i++) + { + this.Values[i] = values[i]; + } + } + finally + { + BytePool256.Return(values, true); + } // Derive the look-up table. for (int i = 0; i < this.Lut.Length; i++) @@ -165,9 +179,9 @@ namespace ImageSharp.Formats.Jpg this.Lut[i] = 0; } - uint x = 0, code = 0; + int x = 0, code = 0; - for (int i = 0; i < LutSize; i++) + for (int i = 0; i < LutSizeLog2; i++) { code <<= 1; @@ -178,8 +192,8 @@ namespace ImageSharp.Formats.Jpg // 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)((this.Values[x] << 8) | (2 + i)); + int base2 = code << (7 - i); + int lutValue = (this.Values[x] << 8) | (2 + i); for (int k = 0; k < 1 << (7 - i); k++) { @@ -215,16 +229,27 @@ namespace ImageSharp.Formats.Jpg } } + /// + /// Gets the value for the given code and index. + /// + /// The code + /// The code length + /// The value + public int GetValue(int code, int codeLength) + { + return this.Values[this.Indices[codeLength] + code - this.MinCodes[codeLength]]; + } + /// /// Initializes the Huffman tree /// private void Init() { - 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); + this.Lut = IntPool256.Rent(MaxNCodes); + this.Values = IntPool256.Rent(MaxNCodes); + this.MinCodes = CodesPool16.Rent(MaxCodeLength); + this.MaxCodes = CodesPool16.Rent(MaxCodeLength); + this.Indices = CodesPool16.Rent(MaxCodeLength); } } } \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/InputProcessor.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/InputProcessor.cs new file mode 100644 index 0000000000..60042d36f8 --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/InputProcessor.cs @@ -0,0 +1,368 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpg +{ + using System; + using System.IO; + using System.Runtime.CompilerServices; + + /// + /// Encapsulates stream reading and processing data and operations for . + /// It's a value type for imporved data locality, and reduced number of CALLVIRT-s + /// + internal struct InputProcessor : IDisposable + { + /// + /// Holds the unprocessed bits that have been taken from the byte-stream. + /// + public Bits Bits; + + /// + /// The byte buffer + /// + public Bytes Bytes; + + /// + /// Initializes a new instance of the struct. + /// + /// The input + /// Temporal buffer, same as + public InputProcessor(Stream inputStream, byte[] temp) + { + this.Bits = default(Bits); + this.Bytes = Bytes.Create(); + this.InputStream = inputStream; + this.Temp = temp; + this.UnexpectedEndOfStreamReached = false; + } + + /// + /// Gets the input stream + /// + public Stream InputStream { get; } + + /// + /// Gets the temporal buffer, same instance as + /// + public byte[] Temp { get; } + + /// + /// Gets or sets a value indicating whether an unexpected EOF reached in . + /// + public bool UnexpectedEndOfStreamReached { get; set; } + + /// + /// If errorCode indicates unexpected EOF, sets to true and returns false. + /// Calls and returns true otherwise. + /// + /// The + /// indicating whether everything is OK + public bool CheckEOFEnsureNoError(DecoderErrorCode errorCode) + { + if (errorCode == DecoderErrorCode.UnexpectedEndOfStream) + { + this.UnexpectedEndOfStreamReached = true; + return false; + } + + errorCode.EnsureNoError(); + return true; + } + + /// + /// If errorCode indicates unexpected EOF, sets to true and returns false. + /// Returns true otherwise. + /// + /// The + /// indicating whether everything is OK + public bool CheckEOF(DecoderErrorCode errorCode) + { + if (errorCode == DecoderErrorCode.UnexpectedEndOfStream) + { + this.UnexpectedEndOfStreamReached = true; + return false; + } + + return true; + } + + /// + /// Dispose + /// + public void Dispose() + { + this.Bytes.Dispose(); + } + + /// + /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() + { + return this.Bytes.ReadByte(this.InputStream); + } + + /// + /// Decodes a single bit + /// TODO: This method (and also the usages) could be optimized by batching! + /// + /// The decoded bit as a + /// The + public DecoderErrorCode DecodeBitUnsafe(out bool result) + { + if (this.Bits.UnreadBits == 0) + { + DecoderErrorCode errorCode = this.Bits.Ensure1BitUnsafe(ref this); + if (errorCode != DecoderErrorCode.NoError) + { + result = false; + return errorCode; + } + } + + result = (this.Bits.Accumulator & this.Bits.Mask) != 0; + this.Bits.UnreadBits--; + this.Bits.Mask >>= 1; + return DecoderErrorCode.NoError; + } + + /// + /// Reads exactly length bytes into data. It does not care about byte stuffing. + /// Does not throw on errors, returns instead! + /// + /// The data to write to. + /// The offset in the source buffer + /// The number of bytes to read + /// The + public DecoderErrorCode ReadFullUnsafe(byte[] data, int offset, int length) + { + // Unread the overshot bytes, if any. + if (this.Bytes.UnreadableBytes != 0) + { + if (this.Bits.UnreadBits >= 8) + { + this.UnreadByteStuffedByte(); + } + + this.Bytes.UnreadableBytes = 0; + } + + DecoderErrorCode errorCode = DecoderErrorCode.NoError; + while (length > 0) + { + if (this.Bytes.J - this.Bytes.I >= length) + { + Array.Copy(this.Bytes.Buffer, this.Bytes.I, data, offset, length); + this.Bytes.I += length; + length -= length; + } + else + { + Array.Copy(this.Bytes.Buffer, this.Bytes.I, data, offset, this.Bytes.J - this.Bytes.I); + offset += this.Bytes.J - this.Bytes.I; + length -= this.Bytes.J - this.Bytes.I; + this.Bytes.I += this.Bytes.J - this.Bytes.I; + + errorCode = this.Bytes.FillUnsafe(this.InputStream); + } + } + + return errorCode; + } + + /// + /// Decodes the given number of bits + /// + /// The number of bits to decode. + /// The result + /// The + public DecoderErrorCode DecodeBitsUnsafe(int count, out int result) + { + if (this.Bits.UnreadBits < count) + { + this.Bits.EnsureNBits(count, ref this); + } + + result = this.Bits.Accumulator >> (this.Bits.UnreadBits - count); + result = result & ((1 << count) - 1); + this.Bits.UnreadBits -= count; + this.Bits.Mask >>= count; + return DecoderErrorCode.NoError; + } + + /// + /// Extracts the next Huffman-coded value from the bit-stream into result, decoded according to the given value. + /// + /// The huffman value + /// The decoded + /// The + public DecoderErrorCode DecodeHuffmanUnsafe(ref HuffmanTree huffmanTree, out int result) + { + result = 0; + + if (huffmanTree.Length == 0) + { + DecoderThrowHelper.ThrowImageFormatException.UninitializedHuffmanTable(); + } + + if (this.Bits.UnreadBits < 8) + { + DecoderErrorCode errorCode = this.Bits.Ensure8BitsUnsafe(ref this); + + if (errorCode == DecoderErrorCode.NoError) + { + int lutIndex = (this.Bits.Accumulator >> (this.Bits.UnreadBits - HuffmanTree.LutSizeLog2)) & 0xFF; + int v = huffmanTree.Lut[lutIndex]; + + if (v != 0) + { + int n = (v & 0xFF) - 1; + this.Bits.UnreadBits -= n; + this.Bits.Mask >>= n; + result = v >> 8; + return errorCode; + } + } + else + { + this.UnreadByteStuffedByte(); + return errorCode; + } + } + + int code = 0; + for (int i = 0; i < HuffmanTree.MaxCodeLength; i++) + { + if (this.Bits.UnreadBits == 0) + { + this.Bits.EnsureNBits(1, ref this); + } + + if ((this.Bits.Accumulator & this.Bits.Mask) != 0) + { + code |= 1; + } + + this.Bits.UnreadBits--; + this.Bits.Mask >>= 1; + + if (code <= huffmanTree.MaxCodes[i]) + { + result = huffmanTree.GetValue(code, i); + return DecoderErrorCode.NoError; + } + + code <<= 1; + } + + // Unrecoverable error, throwing: + DecoderThrowHelper.ThrowImageFormatException.BadHuffmanCode(); + + // DUMMY RETURN! C# doesn't know we have thrown an exception! + return DecoderErrorCode.NoError; + } + + /// + /// Skips the next n bytes. + /// + /// The number of bytes to ignore. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Skip(int count) + { + DecoderErrorCode errorCode = this.SkipUnsafe(count); + errorCode.EnsureNoError(); + } + + /// + /// Skips the next n bytes. + /// Does not throw, returns instead! + /// + /// The number of bytes to ignore. + /// The + public DecoderErrorCode SkipUnsafe(int count) + { + // Unread the overshot bytes, if any. + if (this.Bytes.UnreadableBytes != 0) + { + if (this.Bits.UnreadBits >= 8) + { + this.UnreadByteStuffedByte(); + } + + this.Bytes.UnreadableBytes = 0; + } + + while (true) + { + int m = this.Bytes.J - this.Bytes.I; + if (m > count) + { + m = count; + } + + this.Bytes.I += m; + count -= m; + if (count == 0) + { + break; + } + + DecoderErrorCode errorCode = this.Bytes.FillUnsafe(this.InputStream); + if (errorCode != DecoderErrorCode.NoError) + { + return errorCode; + } + } + + return DecoderErrorCode.NoError; + } + + /// + /// Reads exactly length bytes into data. It does not care about byte stuffing. + /// + /// The data to write to. + /// The offset in the source buffer + /// The number of bytes to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadFull(byte[] data, int offset, int length) + { + DecoderErrorCode errorCode = this.ReadFullUnsafe(data, offset, length); + errorCode.EnsureNoError(); + } + + /// + /// Undoes the most recent ReadByteStuffedByte call, + /// giving a byte of data back from bits to bytes. The Huffman look-up table + /// requires at least 8 bits for look-up, which means that Huffman decoding can + /// sometimes overshoot and read one or two too many bytes. Two-byte overshoot + /// can happen when expecting to read a 0xff 0x00 byte-stuffed byte. + /// + public void UnreadByteStuffedByte() + { + this.Bytes.I -= this.Bytes.UnreadableBytes; + this.Bytes.UnreadableBytes = 0; + if (this.Bits.UnreadBits >= 8) + { + this.Bits.Accumulator >>= 8; + this.Bits.UnreadBits -= 8; + this.Bits.Mask >>= 8; + } + } + + /// + /// Receive extend + /// + /// Byte + /// Read bits value + /// The + public DecoderErrorCode ReceiveExtendUnsafe(int t, out int x) + { + return this.Bits.ReceiveExtendUnsafe(t, ref this, out x); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.ComputationData.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.ComputationData.cs index ef04bf4188..06f170be5a 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.ComputationData.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.ComputationData.cs @@ -13,7 +13,7 @@ namespace ImageSharp.Formats.Jpg internal unsafe partial struct JpegScanDecoder { /// - /// Holds the "large" data blocks needed for computations + /// Holds the "large" data blocks needed for computations. /// [StructLayout(LayoutKind.Sequential)] public struct ComputationData @@ -44,12 +44,12 @@ namespace ImageSharp.Formats.Jpg public UnzigData Unzig; /// - /// The no-idea-what's this data + /// The buffer storing the -s for each component /// public fixed byte ScanData[3 * JpegDecoderCore.MaxComponents]; /// - /// The DC component values + /// The DC values for each component /// public fixed int Dc[JpegDecoderCore.MaxComponents]; diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.cs index 9fef5010df..0e389771c0 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.cs @@ -2,7 +2,6 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - // ReSharper disable InconsistentNaming namespace ImageSharp.Formats.Jpg { @@ -10,20 +9,39 @@ namespace ImageSharp.Formats.Jpg using System.Runtime.CompilerServices; /// - /// Encapsulates the impementation of Jpeg SOS decoder. - /// See JpegScanDecoder.md! + /// Encapsulates the impementation of Jpeg SOS decoder. See JpegScanDecoder.md! + /// TODO: Split JpegScanDecoder: 1. JpegScanDecoder for Huffman-decoding () 2. JpegBlockProcessor for processing () + /// and are the spectral selection bounds. + /// and are the successive approximation high and low values. + /// The spec calls these values Ss, Se, Ah and Al. + /// For progressive JPEGs, these are the two more-or-less independent + /// aspects of progression. Spectral selection progression is when not + /// all of a block's 64 DCT coefficients are transmitted in one pass. + /// For example, three passes could transmit coefficient 0 (the DC + /// component), coefficients 1-5, and coefficients 6-63, in zig-zag + /// order. Successive approximation is when not all of the bits of a + /// band of coefficients are transmitted in one pass. For example, + /// three passes could transmit the 6 most significant bits, followed + /// by the second-least significant bit, followed by the least + /// significant bit. + /// For baseline JPEGs, these parameters are hard-coded to 0/63/0/0. /// internal unsafe partial struct JpegScanDecoder { /// /// The AC table index /// - private const int AcTableIndex = 1; + public const int AcTableIndex = 1; /// /// The DC table index /// - private const int DcTableIndex = 0; + public const int DcTableIndex = 0; + + /// + /// The current component index + /// + public int ComponentIndex; /// /// X coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0)) @@ -35,21 +53,6 @@ namespace ImageSharp.Formats.Jpg /// private int by; - // zigStart and zigEnd are the spectral selection bounds. - // ah and al are the successive approximation high and low values. - // The spec calls these values Ss, Se, Ah and Al. - // For progressive JPEGs, these are the two more-or-less independent - // aspects of progression. Spectral selection progression is when not - // all of a block's 64 DCT coefficients are transmitted in one pass. - // For example, three passes could transmit coefficient 0 (the DC - // component), coefficients 1-5, and coefficients 6-63, in zig-zag - // order. Successive approximation is when not all of the bits of a - // band of coefficients are transmitted in one pass. For example, - // three passes could transmit the 6 most significant bits, followed - // by the second-least significant bit, followed by the least - // significant bit. - // For baseline JPEGs, these parameters are hard-coded to 0/63/0/0. - /// /// Start index of the zig-zag selection bound /// @@ -75,11 +78,6 @@ namespace ImageSharp.Formats.Jpg /// private int componentScanCount; - /// - /// The current component index - /// - private int componentIndex; - /// /// Horizontal sampling factor at the current component index /// @@ -88,36 +86,80 @@ namespace ImageSharp.Formats.Jpg /// /// End-of-Band run, specified in section G.1.2.2. /// - private ushort eobRun; + private int eobRun; /// - /// The buffer + /// Pointers to elements of /// - private ComputationData data; + private DataPointers pointers; /// - /// Pointers to elements of + /// The buffer /// - private DataPointers pointers; + private ComputationData data; /// - /// Initializes the default instance after creation. + /// Initializes a default-constructed instance for reading data from -s stream. /// /// Pointer to on the stack /// The instance /// The remaining bytes in the segment block. - public static void Init(JpegScanDecoder* p, JpegDecoderCore decoder, int remaining) + public static void InitStreamReading(JpegScanDecoder* p, JpegDecoderCore decoder, int remaining) + { + Init(p); + p->InitStreamReadingImpl(decoder, remaining); + } + + /// + /// Initializes a default-constructed instance, filling the data and setting the pointers. + /// + /// Pointer to on the stack + public static void Init(JpegScanDecoder* p) { p->data = ComputationData.Create(); p->pointers = new DataPointers(&p->data); - p->InitImpl(decoder, remaining); } /// - /// Reads the blocks from the -s stream, and processes them into the corresponding instances. + /// Loads the data from the given into the block. + /// + /// The + public void LoadMemento(ref DecodedBlockMemento memento) + { + this.bx = memento.Bx; + this.by = memento.By; + this.data.Block = memento.Block; + } + + /// + /// Read Huffman data from Jpeg scans in , + /// and decode it as into . + /// + /// The blocks are traversed one MCU at a time. For 4:2:0 chroma + /// subsampling, there are four Y 8x8 blocks in every 16x16 MCU. + /// For a baseline 32x16 pixel image, the Y blocks visiting order is: + /// 0 1 4 5 + /// 2 3 6 7 + /// For progressive images, the interleaved scans (those with component count > 1) + /// are traversed as above, but non-interleaved scans are traversed left + /// to right, top to bottom: + /// 0 1 2 3 + /// 4 5 6 7 + /// Only DC scans (zigStart == 0) can be interleave AC scans must have + /// only one component. + /// To further complicate matters, for non-interleaved scans, there is no + /// data for any blocks that are inside the image at the MCU level but + /// outside the image at the pixel level. For example, a 24x16 pixel 4:2:0 + /// progressive image consists of two 16x16 MCUs. The interleaved scans + /// will process 8 Y blocks: + /// 0 1 4 5 + /// 2 3 6 7 + /// The non-interleaved scans will process only 6 Y blocks: + /// 0 1 2 + /// 3 4 5 /// /// The instance - public void ProcessBlocks(JpegDecoderCore decoder) + public void DecodeBlocks(JpegDecoderCore decoder) { int blockCount = 0; int mcu = 0; @@ -127,36 +169,14 @@ namespace ImageSharp.Formats.Jpg { for (int mx = 0; mx < decoder.MCUCountX; mx++) { - for (int i = 0; i < this.componentScanCount; i++) + for (int scanIndex = 0; scanIndex < this.componentScanCount; scanIndex++) { - this.componentIndex = this.pointers.ComponentScan[i].ComponentIndex; - this.hi = decoder.ComponentArray[this.componentIndex].HorizontalFactor; - int vi = decoder.ComponentArray[this.componentIndex].VerticalFactor; + this.ComponentIndex = this.pointers.ComponentScan[scanIndex].ComponentIndex; + this.hi = decoder.ComponentArray[this.ComponentIndex].HorizontalFactor; + int vi = decoder.ComponentArray[this.ComponentIndex].VerticalFactor; for (int j = 0; j < this.hi * vi; j++) { - // The blocks are traversed one MCU at a time. For 4:2:0 chroma - // subsampling, there are four Y 8x8 blocks in every 16x16 MCU. - // For a baseline 32x16 pixel image, the Y blocks visiting order is: - // 0 1 4 5 - // 2 3 6 7 - // For progressive images, the interleaved scans (those with component count > 1) - // are traversed as above, but non-interleaved scans are traversed left - // to right, top to bottom: - // 0 1 2 3 - // 4 5 6 7 - // Only DC scans (zigStart == 0) can be interleave AC scans must have - // only one component. - // To further complicate matters, for non-interleaved scans, there is no - // data for any blocks that are inside the image at the MCU level but - // outside the image at the pixel level. For example, a 24x16 pixel 4:2:0 - // progressive image consists of two 16x16 MCUs. The interleaved scans - // will process 8 Y blocks: - // 0 1 4 5 - // 2 3 6 7 - // The non-interleaved scans will process only 6 Y blocks: - // 0 1 2 - // 3 4 5 if (this.componentScanCount != 1) { this.bx = (this.hi * mx) + (j % this.hi); @@ -174,23 +194,18 @@ namespace ImageSharp.Formats.Jpg } } - int qtIndex = decoder.ComponentArray[this.componentIndex].Selector; + // Take an existing block (required when progressive): + int blockIndex = this.GetBlockIndex(decoder); + this.data.Block = decoder.DecodedBlocks[this.ComponentIndex].Buffer[blockIndex].Block; - // TODO: Reading & processing blocks should be done in 2 separate loops. The second one could be parallelized. The first one could be async. - this.data.QuantiazationTable = decoder.QuantizationTables[qtIndex]; - - // Load the previous partially decoded coefficients, if applicable. - if (decoder.IsProgressive) + if (!decoder.InputProcessor.UnexpectedEndOfStreamReached) { - int blockIndex = this.GetBlockIndex(decoder); - this.data.Block = decoder.DecodedBlocks[this.componentIndex][blockIndex]; - } - else - { - this.data.Block.Clear(); + this.DecodeBlock(decoder, scanIndex); } - this.ProcessBlockImpl(decoder, i); + // Store the decoded block + DecodedBlockMemento.Array blocks = decoder.DecodedBlocks[this.ComponentIndex]; + DecodedBlockMemento.Store(ref blocks, blockIndex, this.bx, this.by, ref this.data.Block); } // for j @@ -203,20 +218,26 @@ namespace ImageSharp.Formats.Jpg { // A more sophisticated decoder could use RST[0-7] markers to resynchronize from corrupt input, // but this one assumes well-formed input, and hence the restart marker follows immediately. - decoder.ReadFull(decoder.Temp, 0, 2); - if (decoder.Temp[0] != 0xff || decoder.Temp[1] != expectedRst) + if (!decoder.InputProcessor.UnexpectedEndOfStreamReached) { - throw new ImageFormatException("Bad RST marker"); - } + DecoderErrorCode errorCode = decoder.InputProcessor.ReadFullUnsafe(decoder.Temp, 0, 2); + if (decoder.InputProcessor.CheckEOFEnsureNoError(errorCode)) + { + if (decoder.Temp[0] != 0xff || decoder.Temp[1] != expectedRst) + { + throw new ImageFormatException("Bad RST marker"); + } - expectedRst++; - if (expectedRst == JpegConstants.Markers.RST7 + 1) - { - expectedRst = JpegConstants.Markers.RST0; + expectedRst++; + if (expectedRst == JpegConstants.Markers.RST7 + 1) + { + expectedRst = JpegConstants.Markers.RST0; + } + } } // Reset the Huffman decoder. - decoder.Bits = default(Bits); + decoder.InputProcessor.Bits = default(Bits); // Reset the DC components, as per section F.2.1.3.1. this.ResetDc(); @@ -230,17 +251,37 @@ namespace ImageSharp.Formats.Jpg } } + /// + /// Dequantize, perform the inverse DCT and store the block to the into the corresponding instances. + /// + /// The instance + public void ProcessBlockColors(JpegDecoderCore decoder) + { + int qtIndex = decoder.ComponentArray[this.ComponentIndex].Selector; + this.data.QuantiazationTable = decoder.QuantizationTables[qtIndex]; + + Block8x8F* b = this.pointers.Block; + + Block8x8F.UnZig(b, this.pointers.QuantiazationTable, this.pointers.Unzig); + + DCT.TransformIDCT(ref *b, ref *this.pointers.Temp1, ref *this.pointers.Temp2); + + var destChannel = decoder.GetDestinationChannel(this.ComponentIndex); + var destArea = destChannel.GetOffsetedSubAreaForBlock(this.bx, this.by); + destArea.LoadColorsFrom(this.pointers.Temp1, this.pointers.Temp2); + } + private void ResetDc() { Unsafe.InitBlock(this.pointers.Dc, default(byte), sizeof(int) * JpegDecoderCore.MaxComponents); } /// - /// The implementation part of as an instance method. + /// The implementation part of as an instance method. /// /// The /// The remaining bytes - private void InitImpl(JpegDecoderCore decoder, int remaining) + private void InitStreamReadingImpl(JpegDecoderCore decoder, int remaining) { if (decoder.ComponentCount == 0) { @@ -252,7 +293,7 @@ namespace ImageSharp.Formats.Jpg throw new ImageFormatException("SOS has wrong length"); } - decoder.ReadFull(decoder.Temp, 0, remaining); + decoder.InputProcessor.ReadFull(decoder.Temp, 0, remaining); this.componentScanCount = decoder.Temp[0]; int scanComponentCountX2 = 2 * this.componentScanCount; @@ -265,7 +306,7 @@ namespace ImageSharp.Formats.Jpg for (int i = 0; i < this.componentScanCount; i++) { - this.ProcessScanImpl(decoder, i, ref this.pointers.ComponentScan[i], ref totalHv); + this.InitComponentScan(decoder, i, ref this.pointers.ComponentScan[i], ref totalHv); } // Section B.2.3 states that if there is more than one component then the @@ -303,18 +344,18 @@ namespace ImageSharp.Formats.Jpg } /// - /// Process the current block at (, ) + /// Read the current the current block at (, ) from the decoders stream /// /// The decoder - /// The index of the scan - private void ProcessBlockImpl(JpegDecoderCore decoder, int i) + /// The index of the scan + private void DecodeBlock(JpegDecoderCore decoder, int scanIndex) { var b = this.pointers.Block; - - int huffmannIdx = (AcTableIndex * HuffmanTree.ThRowSize) + this.pointers.ComponentScan[i].AcTableSelector; + DecoderErrorCode errorCode; + int huffmannIdx = (AcTableIndex * HuffmanTree.ThRowSize) + this.pointers.ComponentScan[scanIndex].AcTableSelector; if (this.ah != 0) { - this.Refine(decoder, ref decoder.HuffmanTrees[huffmannIdx], 1 << this.al); + this.Refine(ref decoder.InputProcessor, ref decoder.HuffmanTrees[huffmannIdx], 1 << this.al); } else { @@ -324,19 +365,32 @@ namespace ImageSharp.Formats.Jpg zig++; // Decode the DC coefficient, as specified in section F.2.2.1. - byte value = - decoder.DecodeHuffman( - ref decoder.HuffmanTrees[(DcTableIndex * HuffmanTree.ThRowSize) + this.pointers.ComponentScan[i].DcTableSelector]); + int value; + int huffmanIndex = (DcTableIndex * HuffmanTree.ThRowSize) + this.pointers.ComponentScan[scanIndex].DcTableSelector; + errorCode = decoder.InputProcessor.DecodeHuffmanUnsafe( + ref decoder.HuffmanTrees[huffmanIndex], + out value); + if (!decoder.InputProcessor.CheckEOF(errorCode)) + { + return; + } + if (value > 16) { throw new ImageFormatException("Excessive DC component"); } - int deltaDC = decoder.Bits.ReceiveExtend(value, decoder); - this.pointers.Dc[this.componentIndex] += deltaDC; + int deltaDC; + errorCode = decoder.InputProcessor.ReceiveExtendUnsafe(value, out deltaDC); + if (!decoder.InputProcessor.CheckEOFEnsureNoError(errorCode)) + { + return; + } + + this.pointers.Dc[this.ComponentIndex] += deltaDC; // b[0] = dc[compIndex] << al; - Block8x8F.SetScalarAt(b, 0, this.pointers.Dc[this.componentIndex] << this.al); + Block8x8F.SetScalarAt(b, 0, this.pointers.Dc[this.ComponentIndex] << this.al); } if (zig <= this.zigEnd && this.eobRun > 0) @@ -348,9 +402,15 @@ namespace ImageSharp.Formats.Jpg // Decode the AC coefficients, as specified in section F.2.2.2. for (; zig <= this.zigEnd; zig++) { - byte value = decoder.DecodeHuffman(ref decoder.HuffmanTrees[huffmannIdx]); - byte val0 = (byte)(value >> 4); - byte val1 = (byte)(value & 0x0f); + int value; + errorCode = decoder.InputProcessor.DecodeHuffmanUnsafe(ref decoder.HuffmanTrees[huffmannIdx], out value); + if (!decoder.InputProcessor.CheckEOF(errorCode)) + { + return; + } + + int val0 = value >> 4; + int val1 = value & 0x0f; if (val1 != 0) { zig += val0; @@ -359,7 +419,12 @@ namespace ImageSharp.Formats.Jpg break; } - int ac = decoder.Bits.ReceiveExtend(val1, decoder); + int ac; + errorCode = decoder.InputProcessor.ReceiveExtendUnsafe(val1, out ac); + if (!decoder.InputProcessor.CheckEOFEnsureNoError(errorCode)) + { + return; + } // b[Unzig[zig]] = ac << al; Block8x8F.SetScalarAt(b, this.pointers.Unzig[zig], ac << this.al); @@ -371,7 +436,11 @@ namespace ImageSharp.Formats.Jpg this.eobRun = (ushort)(1 << val0); if (val0 != 0) { - this.eobRun |= (ushort)decoder.DecodeBits(val0); + errorCode = this.DecodeEobRun(val0, ref decoder.InputProcessor); + if (!decoder.InputProcessor.CheckEOFEnsureNoError(errorCode)) + { + return; + } } this.eobRun--; @@ -383,31 +452,19 @@ namespace ImageSharp.Formats.Jpg } } } + } - if (decoder.IsProgressive) + private DecoderErrorCode DecodeEobRun(int count, ref InputProcessor decoder) + { + int bitsResult; + DecoderErrorCode errorCode = decoder.DecodeBitsUnsafe(count, out bitsResult); + if (errorCode != DecoderErrorCode.NoError) { - if (this.zigEnd != Block8x8F.ScalarCount - 1 || this.al != 0) - { - // We haven't completely decoded this 8x8 block. Save the coefficients. - decoder.DecodedBlocks[this.componentIndex][this.GetBlockIndex(decoder)] = *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. - return; - } + return errorCode; } - // Dequantize, perform the inverse DCT and store the block to the image. - Block8x8F.UnZig(b, this.pointers.QuantiazationTable, this.pointers.Unzig); - - DCT.TransformIDCT(ref *b, ref *this.pointers.Temp1, ref *this.pointers.Temp2); - - var destChannel = decoder.GetDestinationChannel(this.componentIndex); - var destArea = destChannel.GetOffsetedSubAreaForBlock(this.bx, this.by); - destArea.LoadColorsFrom(this.pointers.Temp1, this.pointers.Temp2); + this.eobRun |= bitsResult; + return DecoderErrorCode.NoError; } /// @@ -417,10 +474,10 @@ namespace ImageSharp.Formats.Jpg /// The index private int GetBlockIndex(JpegDecoderCore decoder) { - return ((this.@by * decoder.MCUCountX) * this.hi) + this.bx; + return ((this.by * decoder.MCUCountX) * this.hi) + this.bx; } - private void ProcessScanImpl(JpegDecoderCore decoder, int i, ref ComponentScan currentComponentScan, ref int totalHv) + private void InitComponentScan(JpegDecoderCore decoder, int i, ref ComponentScan currentComponentScan, ref int totalHv) { // Component selector. int cs = decoder.Temp[1 + (2 * i)]; @@ -482,10 +539,10 @@ namespace ImageSharp.Formats.Jpg /// /// Decodes a successive approximation refinement block, as specified in section G.1.2. /// - /// The decoder instance + /// The instance /// The Huffman tree /// The low transform offset - private void Refine(JpegDecoderCore decoder, ref HuffmanTree h, int delta) + private void Refine(ref InputProcessor bp, ref HuffmanTree h, int delta) { Block8x8F* b = this.pointers.Block; @@ -497,7 +554,13 @@ namespace ImageSharp.Formats.Jpg throw new ImageFormatException("Invalid state for zig DC component"); } - bool bit = decoder.DecodeBit(); + bool bit; + DecoderErrorCode errorCode = bp.DecodeBitUnsafe(out bit); + if (!bp.CheckEOFEnsureNoError(errorCode)) + { + return; + } + if (bit) { int stuff = (int)Block8x8F.GetScalarAt(b, 0); @@ -520,7 +583,14 @@ namespace ImageSharp.Formats.Jpg { bool done = false; int z = 0; - byte val = decoder.DecodeHuffman(ref h); + + int val; + DecoderErrorCode errorCode = bp.DecodeHuffmanUnsafe(ref h, out val); + if (!bp.CheckEOF(errorCode)) + { + return; + } + int val0 = val >> 4; int val1 = val & 0x0f; @@ -529,10 +599,14 @@ namespace ImageSharp.Formats.Jpg case 0: if (val0 != 0x0f) { - this.eobRun = (ushort)(1 << val0); + this.eobRun = 1 << val0; if (val0 != 0) { - this.eobRun |= (ushort)decoder.DecodeBits(val0); + errorCode = this.DecodeEobRun(val0, ref bp); + if (!bp.CheckEOFEnsureNoError(errorCode)) + { + return; + } } done = true; @@ -541,7 +615,14 @@ namespace ImageSharp.Formats.Jpg break; case 1: z = delta; - bool bit = decoder.DecodeBit(); + + bool bit; + errorCode = bp.DecodeBitUnsafe(out bit); + if (!bp.CheckEOFEnsureNoError(errorCode)) + { + return; + } + if (!bit) { z = -z; @@ -557,7 +638,12 @@ namespace ImageSharp.Formats.Jpg break; } - zig = this.RefineNonZeroes(decoder, zig, val0, delta); + zig = this.RefineNonZeroes(ref bp, zig, val0, delta); + if (bp.UnexpectedEndOfStreamReached) + { + return; + } + if (zig > this.zigEnd) { throw new ImageFormatException($"Too many coefficients {zig} > {this.zigEnd}"); @@ -574,7 +660,7 @@ namespace ImageSharp.Formats.Jpg if (this.eobRun > 0) { this.eobRun--; - this.RefineNonZeroes(decoder, zig, -1, delta); + this.RefineNonZeroes(ref bp, zig, -1, delta); } } @@ -582,12 +668,12 @@ namespace ImageSharp.Formats.Jpg /// Refines non-zero entries of b in zig-zag order. /// If >= 0, the first zero entries are skipped over. /// - /// The decoder + /// The /// The zig-zag start index /// The non-zero entry /// The low transform offset /// The - private int RefineNonZeroes(JpegDecoderCore decoder, int zig, int nz, int delta) + private int RefineNonZeroes(ref InputProcessor bp, int zig, int nz, int delta) { var b = this.pointers.Block; for (; zig <= this.zigEnd; zig++) @@ -607,7 +693,13 @@ namespace ImageSharp.Formats.Jpg continue; } - bool bit = decoder.DecodeBit(); + bool bit; + DecoderErrorCode errorCode = bp.DecodeBitUnsafe(out bit); + if (!bp.CheckEOFEnsureNoError(errorCode)) + { + return int.MinValue; + } + if (!bit) { continue; diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.md b/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.md index 215f21807b..4ca4d1f642 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.md +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/JpegScanDecoder.md @@ -4,7 +4,7 @@ The implementation is optimized to hold most of the necessary data in a single v #### Benefits: - Maximized locality of reference by keeping most of the operation data on the stack -- Reaching this without long parameter lists, most of the values describing the state of the decoder algorithm +- Achieving this without long parameter lists, most of the values describing the state of the decoder algorithm are members of the `JpegScanDecoder` struct - Most of the logic related to Scan decoding is refactored & simplified now to live in the methods of `JpegScanDecoder` - The first step is done towards separating the stream reading from block processing. They can be refactored later to be executed in two disctinct loops. @@ -16,8 +16,8 @@ are members of the `JpegScanDecoder` struct |JpegScanDecoder | |-------------------| |Variables | -|ComputationData | |DataPointers | +|ComputationData | - **ComputationData** holds the "large" data blocks needed for computations (Mostly `Block8x8F`-s) - **DataPointers** contains pointers to the memory regions of `ComponentData` so they can be easily passed around to pointer based utility methods of `Block8x8F` diff --git a/src/ImageSharp.Formats.Jpeg/Components/Decoder/MissingFF00Exception.cs b/src/ImageSharp.Formats.Jpeg/Components/Decoder/MissingFF00Exception.cs new file mode 100644 index 0000000000..f8c157237d --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/Components/Decoder/MissingFF00Exception.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpg +{ + using System; + + /// + /// The missing ff00 exception. + /// + // ReSharper disable once InconsistentNaming + internal class MissingFF00Exception : Exception + { + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/JpegConstants.cs b/src/ImageSharp.Formats.Jpeg/JpegConstants.cs index 19d726e708..74f9a3c07d 100644 --- a/src/ImageSharp.Formats.Jpeg/JpegConstants.cs +++ b/src/ImageSharp.Formats.Jpeg/JpegConstants.cs @@ -86,6 +86,11 @@ namespace ImageSharp.Formats /// public const byte XFF = 0xff; + /// + /// Same as but of type + /// + public const int XFFInt = XFF; + /// /// Start of Image /// diff --git a/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs b/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs index 707f9d3e4d..eca4d46229 100644 --- a/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs @@ -5,8 +5,10 @@ namespace ImageSharp.Formats { using System; + using System.Buffers; using System.IO; using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; using System.Threading.Tasks; using ImageSharp.Formats.Jpg; @@ -21,23 +23,20 @@ namespace ImageSharp.Formats /// public const int MaxComponents = 4; - // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P -#pragma warning disable SA1401 // FieldsMustBePrivate /// - /// Holds the unprocessed bits that have been taken from the byte-stream. + /// The maximum number of quantization tables /// - public Bits Bits; + public const int MaxTq = 3; - /// - /// The byte buffer. - /// - public Bytes Bytes; -#pragma warning restore SA401 + // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P +#pragma warning disable SA1401 // FieldsMustBePrivate /// - /// The maximum number of quantization tables + /// Encapsulates stream reading and processing data and operations for . + /// It's a value type for imporved data locality, and reduced number of CALLVIRT-s /// - private const int MaxTq = 3; + public InputProcessor InputProcessor; +#pragma warning restore SA401 /// /// The App14 marker color-space @@ -88,27 +87,7 @@ namespace ImageSharp.Formats this.QuantizationTables = new Block8x8F[MaxTq + 1]; this.Temp = new byte[2 * Block8x8F.ScalarCount]; this.ComponentArray = new Component[MaxComponents]; - this.DecodedBlocks = new Block8x8F[MaxComponents][]; - this.Bits = default(Bits); - this.Bytes = Bytes.Create(); - } - - /// - /// ReadByteStuffedByte was throwing exceptions on normal execution path (very inefficent) - /// It's better tho have an error code for this! - /// - internal enum ErrorCodes - { - /// - /// NoError - /// - NoError, - - /// - /// MissingFF00 - /// - // ReSharper disable once InconsistentNaming - MissingFF00 + this.DecodedBlocks = new DecodedBlockMemento.Array[MaxComponents]; } /// @@ -123,9 +102,9 @@ namespace ImageSharp.Formats /// /// Gets the saved state between progressive-mode scans. - /// TODO: Also store non-progressive data here. (Helps splitting and parallelizing JpegScanDecoder-s loop) + /// TODO: Also save non-progressive data here. (Helps splitting and parallelizing JpegScanDecoder-s loop) /// - public Block8x8F[][] DecodedBlocks { get; } + public DecodedBlockMemento.Array[] DecodedBlocks { get; } /// /// Gets the quantization tables, in zigzag order. @@ -184,20 +163,118 @@ namespace ImageSharp.Formats public int TotalMCUCount => this.MCUCountX * this.MCUCountY; /// - /// Decodes the image from the specified this._stream and sets + /// Decodes the image from the specified and sets /// the data to image. /// /// The pixel format. /// The image, where the data should be set to. /// The stream, where the image should be. - /// Whether to decode metadata only. - public void Decode(Image image, Stream stream, bool configOnly) + /// Whether to decode metadata only. + public void Decode(Image image, Stream stream, bool metadataOnly) + where TColor : struct, IPackedPixel, IEquatable + { + this.ProcessStream(image, stream, metadataOnly); + if (!metadataOnly) + { + this.ProcessBlocksIntoJpegImageChannels(); + this.ConvertJpegPixelsToImagePixels(image); + } + } + + /// + /// Dispose + /// + public void Dispose() + { + for (int i = 0; i < this.HuffmanTrees.Length; i++) + { + this.HuffmanTrees[i].Dispose(); + } + + foreach (DecodedBlockMemento.Array blockArray in this.DecodedBlocks) + { + blockArray.Dispose(); + } + + this.ycbcrImage?.Dispose(); + this.InputProcessor.Dispose(); + this.grayImage.ReturnPooled(); + this.blackImage.ReturnPooled(); + } + + /// + /// Gets the representing the channel at a given component index + /// + /// The component index + /// The of the channel + public JpegPixelArea GetDestinationChannel(int compIndex) + { + if (this.ComponentCount == 1) + { + return this.grayImage; + } + else + { + switch (compIndex) + { + case 0: + return this.ycbcrImage.YChannel; + case 1: + return this.ycbcrImage.CbChannel; + case 2: + return this.ycbcrImage.CrChannel; + case 3: + return this.blackImage; + default: + throw new ImageFormatException("Too many components"); + } + } + } + + /// + /// Optimized method to pack bytes to the image from the YCbCr color space. + /// This is faster than implicit casting as it avoids double packing. + /// + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) + where TColor : struct, IPackedPixel, IEquatable + { + int ccb = cb - 128; + int ccr = cr - 128; + + // Speed up the algorithm by removing floating point calculation + // Scale by 65536, add .5F and truncate value. We use bit shifting to divide the result + int r0 = 91881 * ccr; // (1.402F * 65536) + .5F + int g0 = 22554 * ccb; // (0.34414F * 65536) + .5F + int g1 = 46802 * ccr; // (0.71414F * 65536) + .5F + int b0 = 116130 * ccb; // (1.772F * 65536) + .5F + + byte r = (byte)(y + (r0 >> 16)).Clamp(0, 255); + byte g = (byte)(y - (g0 >> 16) - (g1 >> 16)).Clamp(0, 255); + byte b = (byte)(y + (b0 >> 16)).Clamp(0, 255); + packed.PackFromBytes(r, g, b, 255); + } + + /// + /// Read metadata from stream and read the blocks in the scans into . + /// + /// The pixel type + /// The + /// The stream + /// Whether to decode metadata only. + private void ProcessStream(Image image, Stream stream, bool metadataOnly) where TColor : struct, IPackedPixel, IEquatable { this.InputStream = stream; + this.InputProcessor = new InputProcessor(stream, this.Temp); // Check for the Start Of Image marker. - this.ReadFull(this.Temp, 0, 2); + this.InputProcessor.ReadFull(this.Temp, 0, 2); if (this.Temp[0] != JpegConstants.Markers.XFF || this.Temp[1] != JpegConstants.Markers.SOI) { throw new ImageFormatException("Missing SOI marker."); @@ -209,7 +286,7 @@ namespace ImageSharp.Formats // we can't currently short circute progressive images so don't try. while (processBytes) { - this.ReadFull(this.Temp, 0, 2); + this.InputProcessor.ReadFull(this.Temp, 0, 2); while (this.Temp[0] != 0xff) { // Strictly speaking, this is a format error. However, libjpeg is @@ -230,7 +307,7 @@ namespace ImageSharp.Formats // Note that extraneous 0xff bytes in e.g. SOS data are escaped as // "\xff\x00", and so are detected a little further down below. this.Temp[0] = this.Temp[1]; - this.Temp[1] = this.ReadByte(); + this.Temp[1] = this.InputProcessor.ReadByte(); } byte marker = this.Temp[1]; @@ -244,7 +321,7 @@ namespace ImageSharp.Formats { // Section B.1.1.2 says, "Any marker may optionally be preceded by any // number of fill bytes, which are bytes assigned code X'FF'". - marker = this.ReadByte(); + marker = this.InputProcessor.ReadByte(); } // End Of Image. @@ -266,7 +343,7 @@ namespace ImageSharp.Formats // Read the 16-bit length of the segment. The value includes the 2 bytes for the // length itself, so we subtract 2 to get the number of remaining bytes. - this.ReadFull(this.Temp, 0, 2); + this.InputProcessor.ReadFull(this.Temp, 0, 2); int remaining = (this.Temp[0] << 8) + this.Temp[1] - 2; if (remaining < 0) { @@ -280,16 +357,16 @@ namespace ImageSharp.Formats case JpegConstants.Markers.SOF2: this.IsProgressive = marker == JpegConstants.Markers.SOF2; this.ProcessStartOfFrameMarker(remaining); - if (configOnly && this.isJfif) + if (metadataOnly && this.isJfif) { return; } break; case JpegConstants.Markers.DHT: - if (configOnly) + if (metadataOnly) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); } else { @@ -298,9 +375,9 @@ namespace ImageSharp.Formats break; case JpegConstants.Markers.DQT: - if (configOnly) + if (metadataOnly) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); } else { @@ -309,7 +386,7 @@ namespace ImageSharp.Formats break; case JpegConstants.Markers.SOS: - if (configOnly) + if (metadataOnly) { return; } @@ -317,17 +394,17 @@ namespace ImageSharp.Formats // when this is a progressive image this gets called a number of times // need to know how many times this should be called in total. this.ProcessStartOfScan(remaining); - if (!this.IsProgressive) + if (this.InputProcessor.UnexpectedEndOfStreamReached || !this.IsProgressive) { - // if this is not a progressive image we can stop processing bytes as we now have the image data. + // if unexpeced EOF reached or this is not a progressive image we can stop processing bytes as we now have the image data. processBytes = false; } break; case JpegConstants.Markers.DRI: - if (configOnly) + if (metadataOnly) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); } else { @@ -348,7 +425,7 @@ namespace ImageSharp.Formats if ((marker >= JpegConstants.Markers.APP0 && marker <= JpegConstants.Markers.APP15) || marker == JpegConstants.Markers.COM) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); } else if (marker < JpegConstants.Markers.SOF0) { @@ -363,7 +440,60 @@ namespace ImageSharp.Formats break; } } + } + + /// + /// Processes the SOS (Start of scan marker). + /// + /// The remaining bytes in the segment block. + /// + /// Missing SOF Marker + /// SOS has wrong length + /// + private void ProcessStartOfScan(int remaining) + { + JpegScanDecoder scan = default(JpegScanDecoder); + JpegScanDecoder.InitStreamReading(&scan, this, remaining); + this.InputProcessor.Bits = default(Bits); + this.MakeImage(); + scan.DecodeBlocks(this); + } + + /// + /// Process the blocks in into Jpeg image channels ( and ) + /// The blocks are expected in a "raw" frequency-domain decoded format. We need to apply IDCT and unzigging to transform them into color-space blocks. + /// We can copy these blocks into -s afterwards. + /// + /// The pixel type + private void ProcessBlocksIntoJpegImageChannels() + where TColor : struct, IPackedPixel, IEquatable + { + Parallel.For( + 0, + this.ComponentCount, + componentIndex => + { + JpegScanDecoder scanDecoder = default(JpegScanDecoder); + JpegScanDecoder.Init(&scanDecoder); + + scanDecoder.ComponentIndex = componentIndex; + DecodedBlockMemento.Array blockArray = this.DecodedBlocks[componentIndex]; + for (int i = 0; i < blockArray.Count; i++) + { + scanDecoder.LoadMemento(ref blockArray.Buffer[i]); + scanDecoder.ProcessBlockColors(this); + } + }); + } + /// + /// Convert the pixel data in and/or into pixels of + /// + /// The pixel type + /// The destination image + private void ConvertJpegPixelsToImagePixels(Image image) + where TColor : struct, IPackedPixel, IEquatable + { if (this.grayImage.IsInitialized) { this.ConvertFromGrayScale(this.ImageWidth, this.ImageHeight, image); @@ -375,7 +505,7 @@ namespace ImageSharp.Formats if (!this.adobeTransformValid) { throw new ImageFormatException( - "Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + "Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); } // See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe @@ -414,239 +544,6 @@ namespace ImageSharp.Formats } } - /// - /// Dispose - /// - public void Dispose() - { - for (int i = 0; i < this.HuffmanTrees.Length; i++) - { - this.HuffmanTrees[i].Dispose(); - } - - this.ycbcrImage?.Dispose(); - this.Bytes.Dispose(); - this.grayImage.ReturnPooled(); - this.blackImage.ReturnPooled(); - } - - /// - /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. - /// - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte ReadByte() - { - return this.Bytes.ReadByte(this.InputStream); - } - - /// - /// Decodes a single bit - /// - /// The - public bool DecodeBit() - { - if (this.Bits.UnreadBits == 0) - { - ErrorCodes errorCode = this.Bits.EnsureNBits(1, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } - } - - bool ret = (this.Bits.Accumulator & this.Bits.Mask) != 0; - this.Bits.UnreadBits--; - this.Bits.Mask >>= 1; - return ret; - } - - /// - /// Reads exactly length bytes into data. It does not care about byte stuffing. - /// - /// The data to write to. - /// The offset in the source buffer - /// The number of bytes to read - public void ReadFull(byte[] data, int offset, int length) - { - // Unread the overshot bytes, if any. - if (this.Bytes.UnreadableBytes != 0) - { - if (this.Bits.UnreadBits >= 8) - { - this.UnreadByteStuffedByte(); - } - - this.Bytes.UnreadableBytes = 0; - } - - while (length > 0) - { - if (this.Bytes.J - this.Bytes.I >= length) - { - Array.Copy(this.Bytes.Buffer, this.Bytes.I, data, offset, length); - this.Bytes.I += length; - length -= length; - } - else - { - Array.Copy(this.Bytes.Buffer, this.Bytes.I, data, offset, this.Bytes.J - this.Bytes.I); - offset += this.Bytes.J - this.Bytes.I; - length -= this.Bytes.J - this.Bytes.I; - this.Bytes.I += this.Bytes.J - this.Bytes.I; - - this.Bytes.Fill(this.InputStream); - } - } - } - - /// - /// Decodes the given number of bits - /// - /// The number of bits to decode. - /// The - public uint DecodeBits(int count) - { - if (this.Bits.UnreadBits < count) - { - ErrorCodes errorCode = this.Bits.EnsureNBits(count, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } - } - - uint ret = this.Bits.Accumulator >> (this.Bits.UnreadBits - count); - ret = (uint)(ret & ((1 << count) - 1)); - this.Bits.UnreadBits -= count; - this.Bits.Mask >>= count; - return ret; - } - - /// - /// Returns the next Huffman-coded value from the bit-stream, decoded according to the given value. - /// - /// The huffman value - /// The - public byte DecodeHuffman(ref HuffmanTree huffmanTree) - { - // Copy stuff to the stack: - if (huffmanTree.Length == 0) - { - throw new ImageFormatException("Uninitialized Huffman table"); - } - - if (this.Bits.UnreadBits < 8) - { - ErrorCodes errorCode = this.Bits.EnsureNBits(8, this); - - if (errorCode == ErrorCodes.NoError) - { - ushort v = huffmanTree.Lut[(this.Bits.Accumulator >> (this.Bits.UnreadBits - HuffmanTree.LutSize)) & 0xFF]; - - if (v != 0) - { - int n = (v & 0xFF) - 1; - this.Bits.UnreadBits -= n; - this.Bits.Mask >>= n; - return (byte)(v >> 8); - } - } - else - { - this.UnreadByteStuffedByte(); - } - } - - int code = 0; - for (int i = 0; i < HuffmanTree.MaxCodeLength; i++) - { - if (this.Bits.UnreadBits == 0) - { - ErrorCodes errorCode = this.Bits.EnsureNBits(1, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } - } - - if ((this.Bits.Accumulator & this.Bits.Mask) != 0) - { - code |= 1; - } - - this.Bits.UnreadBits--; - this.Bits.Mask >>= 1; - - if (code <= huffmanTree.MaxCodes[i]) - { - return huffmanTree.Values[huffmanTree.Indices[i] + code - huffmanTree.MinCodes[i]]; - } - - code <<= 1; - } - - throw new ImageFormatException("Bad Huffman code"); - } - - /// - /// Gets the representing the channel at a given component index - /// - /// The component index - /// The of the channel - public JpegPixelArea GetDestinationChannel(int compIndex) - { - if (this.ComponentCount == 1) - { - return this.grayImage; - } - else - { - switch (compIndex) - { - case 0: - return this.ycbcrImage.YChannel; - case 1: - return this.ycbcrImage.CbChannel; - case 2: - return this.ycbcrImage.CrChannel; - case 3: - return this.blackImage; - default: - throw new ImageFormatException("Too many components"); - } - } - } - - /// - /// Optimized method to pack bytes to the image from the YCbCr color space. - /// This is faster than implicit casting as it avoids double packing. - /// - /// The pixel format. - /// The packed pixel. - /// The y luminance component. - /// The cb chroma component. - /// The cr chroma component. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) - where TColor : struct, IPackedPixel, IEquatable - { - int ccb = cb - 128; - int ccr = cr - 128; - - // Speed up the algorithm by removing floating point calculation - // Scale by 65536, add .5F and truncate value. We use bit shifting to divide the result - int r0 = 91881 * ccr; // (1.402F * 65536) + .5F - int g0 = 22554 * ccb; // (0.34414F * 65536) + .5F - int g1 = 46802 * ccr; // (0.71414F * 65536) + .5F - int b0 = 116130 * ccb; // (1.772F * 65536) + .5F - - byte r = (byte)(y + (r0 >> 16)).Clamp(0, 255); - byte g = (byte)(y - (g0 >> 16) - (g1 >> 16)).Clamp(0, 255); - byte b = (byte)(y + (b0 >> 16)).Clamp(0, 255); - packed.PackFromBytes(r, g, b, 255); - } - /// /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header. /// @@ -1020,11 +917,11 @@ namespace ImageSharp.Formats { if (remaining < 12) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); return; } - this.ReadFull(this.Temp, 0, 12); + this.InputProcessor.ReadFull(this.Temp, 0, 12); remaining -= 12; if (this.Temp[0] == 'A' && this.Temp[1] == 'd' && this.Temp[2] == 'o' && this.Temp[3] == 'b' @@ -1036,7 +933,7 @@ namespace ImageSharp.Formats if (remaining > 0) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); } } @@ -1051,12 +948,12 @@ namespace ImageSharp.Formats { if (remaining < 6) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); return; } byte[] profile = new byte[remaining]; - this.ReadFull(profile, 0, remaining); + this.InputProcessor.ReadFull(profile, 0, remaining); if (profile[0] == 'E' && profile[1] == 'x' && profile[2] == 'i' && profile[3] == 'f' && profile[4] == '\0' && profile[5] == '\0') @@ -1073,11 +970,11 @@ namespace ImageSharp.Formats { if (remaining < 5) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); return; } - this.ReadFull(this.Temp, 0, 13); + this.InputProcessor.ReadFull(this.Temp, 0, 13); remaining -= 13; // TODO: We should be using constants for this. @@ -1092,7 +989,7 @@ namespace ImageSharp.Formats if (remaining > 0) { - this.Skip(remaining); + this.InputProcessor.Skip(remaining); } } @@ -1110,7 +1007,7 @@ namespace ImageSharp.Formats throw new ImageFormatException("DHT has wrong length"); } - this.ReadFull(this.Temp, 0, 17); + this.InputProcessor.ReadFull(this.Temp, 0, 17); int tc = this.Temp[0] >> 4; if (tc > HuffmanTree.MaxTc) @@ -1125,7 +1022,10 @@ namespace ImageSharp.Formats } int huffTreeIndex = (tc * HuffmanTree.ThRowSize) + th; - this.HuffmanTrees[huffTreeIndex].ProcessDefineHuffmanTablesMarkerLoop(this, this.Temp, ref remaining); + this.HuffmanTrees[huffTreeIndex].ProcessDefineHuffmanTablesMarkerLoop( + ref this.InputProcessor, + this.Temp, + ref remaining); } } @@ -1141,7 +1041,7 @@ namespace ImageSharp.Formats throw new ImageFormatException("DRI has wrong length"); } - this.ReadFull(this.Temp, 0, remaining); + this.InputProcessor.ReadFull(this.Temp, 0, remaining); this.RestartInterval = ((int)this.Temp[0] << 8) + (int)this.Temp[1]; } @@ -1159,7 +1059,7 @@ namespace ImageSharp.Formats bool done = false; remaining--; - byte x = this.ReadByte(); + byte x = this.InputProcessor.ReadByte(); int tq = x & 0x0F; if (tq > MaxTq) { @@ -1176,7 +1076,7 @@ namespace ImageSharp.Formats } remaining -= Block8x8F.ScalarCount; - this.ReadFull(this.Temp, 0, Block8x8F.ScalarCount); + this.InputProcessor.ReadFull(this.Temp, 0, Block8x8F.ScalarCount); for (int i = 0; i < Block8x8F.ScalarCount; i++) { @@ -1192,7 +1092,7 @@ namespace ImageSharp.Formats } remaining -= 2 * Block8x8F.ScalarCount; - this.ReadFull(this.Temp, 0, 2 * Block8x8F.ScalarCount); + this.InputProcessor.ReadFull(this.Temp, 0, 2 * Block8x8F.ScalarCount); for (int i = 0; i < Block8x8F.ScalarCount; i++) { @@ -1242,7 +1142,7 @@ namespace ImageSharp.Formats throw new ImageFormatException("Incorrect number of components"); } - this.ReadFull(this.Temp, 0, remaining); + this.InputProcessor.ReadFull(this.Temp, 0, remaining); // We only support 8-bit precision. if (this.Temp[0] != 8) @@ -1411,100 +1311,13 @@ namespace ImageSharp.Formats this.MCUCountX = (this.ImageWidth + (8 * h0) - 1) / (8 * h0); this.MCUCountY = (this.ImageHeight + (8 * v0) - 1) / (8 * v0); + // As a preparation for parallelizing Scan decoder, we also allocate DecodedBlocks in the non-progressive case! for (int i = 0; i < this.ComponentCount; i++) { - int size = this.TotalMCUCount * this.ComponentArray[i].HorizontalFactor + int count = this.TotalMCUCount * this.ComponentArray[i].HorizontalFactor * this.ComponentArray[i].VerticalFactor; - this.DecodedBlocks[i] = new Block8x8F[size]; - } - } - - /// - /// Processes the SOS (Start of scan marker). - /// - /// The remaining bytes in the segment block. - /// - /// Missing SOF Marker - /// SOS has wrong length - /// - private void ProcessStartOfScan(int remaining) - { - JpegScanDecoder scan = default(JpegScanDecoder); - JpegScanDecoder.Init(&scan, this, remaining); - this.Bits = default(Bits); - this.MakeImage(); - scan.ProcessBlocks(this); - } - - /// - /// Skips the next n bytes. - /// - /// The number of bytes to ignore. - private void Skip(int count) - { - // Unread the overshot bytes, if any. - if (this.Bytes.UnreadableBytes != 0) - { - if (this.Bits.UnreadBits >= 8) - { - this.UnreadByteStuffedByte(); - } - - this.Bytes.UnreadableBytes = 0; + this.DecodedBlocks[i] = new DecodedBlockMemento.Array(count); } - - while (true) - { - int m = this.Bytes.J - this.Bytes.I; - if (m > count) - { - m = count; - } - - this.Bytes.I += m; - count -= m; - if (count == 0) - { - break; - } - - this.Bytes.Fill(this.InputStream); - } - } - - /// - /// Undoes the most recent ReadByteStuffedByte call, - /// giving a byte of data back from bits to bytes. The Huffman look-up table - /// requires at least 8 bits for look-up, which means that Huffman decoding can - /// sometimes overshoot and read one or two too many bytes. Two-byte overshoot - /// can happen when expecting to read a 0xff 0x00 byte-stuffed byte. - /// - private void UnreadByteStuffedByte() - { - this.Bytes.I -= this.Bytes.UnreadableBytes; - this.Bytes.UnreadableBytes = 0; - if (this.Bits.UnreadBits >= 8) - { - this.Bits.Accumulator >>= 8; - this.Bits.UnreadBits -= 8; - this.Bits.Mask >>= 8; - } - } - - /// - /// The EOF (End of File exception). - /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker - /// - internal class EOFException : Exception - { - } - - /// - /// The missing ff00 exception. - /// - // ReSharper disable once InconsistentNaming - internal class MissingFF00Exception : Exception - { } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs b/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs index 9007ef75b6..26aad07b8d 100644 --- a/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs +++ b/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs @@ -86,6 +86,10 @@ namespace ImageSharp.Benchmarks.Image [Setup] public void ReadImages() { + if (!Vector.IsHardwareAccelerated) + { + throw new Exception("Vector.IsHardwareAccelerated == false! Check your build settings!"); + } // Console.WriteLine("Vector.IsHardwareAccelerated: " + Vector.IsHardwareAccelerated); this.ReadFilesImpl(); } diff --git a/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj b/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj index 5a27190dbf..305fac6369 100644 --- a/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj +++ b/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj @@ -6,7 +6,7 @@ Debug AnyCPU {96188137-5FA6-4924-AB6E-4EFF79C6E0BB} - Library + Exe Properties ImageSharp ImageSharp.Sandbox46 @@ -24,6 +24,7 @@ prompt 4 true + false pdbonly @@ -33,17 +34,127 @@ prompt 4 true + false + + + ImageSharp.Sandbox46.Program + + ..\..\packages\BenchmarkDotNet.0.10.2\lib\net45\BenchmarkDotNet.dll + True + + + ..\..\packages\BenchmarkDotNet.Core.0.10.2\lib\net45\BenchmarkDotNet.Core.dll + True + + + ..\..\packages\BenchmarkDotNet.Diagnostics.Windows.0.10.2\lib\net45\BenchmarkDotNet.Diagnostics.Windows.dll + True + + + ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.2\lib\net45\BenchmarkDotNet.Toolchains.Roslyn.dll + True + + + ..\..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll + True + + + ..\..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.1.0.41\lib\net40\Microsoft.Diagnostics.Tracing.TraceEvent.dll + True + + + ..\..\packages\System.AppContext.4.1.0\lib\net46\System.AppContext.dll + True + + + ..\..\packages\System.Collections.Immutable.1.2.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + ..\..\packages\System.Console.4.0.0\lib\net46\System.Console.dll + True + + + ..\..\packages\System.Diagnostics.FileVersionInfo.4.0.0\lib\net46\System.Diagnostics.FileVersionInfo.dll + True + + + ..\..\packages\System.Diagnostics.StackTrace.4.0.1\lib\net46\System.Diagnostics.StackTrace.dll + True + + + ..\..\packages\System.IO.FileSystem.4.0.1\lib\net46\System.IO.FileSystem.dll + True + + + ..\..\packages\System.IO.FileSystem.Primitives.4.0.1\lib\net46\System.IO.FileSystem.Primitives.dll + True + + + + ..\..\packages\System.Numerics.Vectors.4.1.1\lib\net46\System.Numerics.Vectors.dll + True + + + ..\..\packages\System.Reflection.Metadata.1.3.0\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + ..\..\packages\System.Security.Cryptography.Algorithms.4.2.0\lib\net461\System.Security.Cryptography.Algorithms.dll + True + + + ..\..\packages\System.Security.Cryptography.Encoding.4.0.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + + + ..\..\packages\System.Security.Cryptography.Primitives.4.0.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + + + ..\..\packages\System.Security.Cryptography.X509Certificates.4.1.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + True + + + ..\..\packages\System.Text.Encoding.CodePages.4.0.1\lib\net46\System.Text.Encoding.CodePages.dll + True + + + ..\..\packages\System.Threading.Tasks.Extensions.4.0.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + True + + + ..\..\packages\System.Threading.Thread.4.0.0\lib\net46\System.Threading.Thread.dll + True + + + ..\..\packages\System.Xml.XmlDocument.4.0.1\lib\net46\System.Xml.XmlDocument.dll + True + + + ..\..\packages\System.Xml.XPath.4.0.1\lib\net46\System.Xml.XPath.dll + True + + + ..\..\packages\System.Xml.XPath.XDocument.4.0.1\lib\net46\System.Xml.XPath.XDocument.dll + True + ..\..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll True @@ -199,22 +310,33 @@ Tests\TestUtilities\TestUtilityExtensions.cs + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + +