From 8b699bf1d41586718097a144bfa01c7e7322e2e6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 22 Jun 2017 15:40:18 +1000 Subject: [PATCH] Can now decode a scan --- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 6 +- .../Jpeg/Port/Components/FileMarker.cs | 53 ++ .../Formats/Jpeg/Port/Components/Frame.cs | 41 +- .../{Component.cs => FrameComponent.cs} | 23 +- .../Jpeg/Port/Components/HuffmanBranch.cs | 21 +- .../Jpeg/Port/Components/HuffmanTables.cs | 6 +- .../Port/Components/QuantizationTables.cs | 42 +- .../Jpeg/Port/Components/ScanDecoder.cs | 471 ++++++++++++++++++ .../Formats/Jpeg/Port/JpegConstants.cs | 18 + .../Formats/Jpeg/Port/JpegDecoderCore.cs | 261 ++++++---- .../ImageSharp.Benchmarks/Image/DecodeJpeg.cs | 24 +- 11 files changed, 828 insertions(+), 138 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs rename src/ImageSharp/Formats/Jpeg/Port/Components/{Component.cs => FrameComponent.cs} (75%) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index e900e51ac8..117edb2254 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -25,8 +25,10 @@ namespace ImageSharp.Formats // { // return decoder.Decode(stream); // } - var decoder = new Jpeg.Port.JpegDecoderCore(options, configuration); - return decoder.Decode(stream); + using (var decoder = new Jpeg.Port.JpegDecoderCore(options, configuration)) + { + return decoder.Decode(stream); + } } } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs new file mode 100644 index 0000000000..39adba5cde --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Represents a jpeg file marker + /// + internal struct FileMarker + { + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + public FileMarker(ushort marker, long position) + { + this.Marker = marker; + this.Position = position; + this.Invalid = false; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + /// Whether the current marker is invalid + public FileMarker(ushort marker, long position, bool invalid) + { + this.Marker = marker; + this.Position = position; + this.Invalid = invalid; + } + + /// + /// Gets or sets a value indicating whether the current marker is invalid + /// + public bool Invalid { get; set; } + + /// + /// Gets the position of the marker within a stream + /// + public ushort Marker { get; } + + /// + /// Gets the position of the marker within a stream + /// + public long Position { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs index 7e72df8b0a..97c422ca32 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs @@ -5,11 +5,15 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; + /// /// Represent a single jpeg frame /// - internal class Frame + internal class Frame : IDisposable { + private bool isDisposed; + /// /// Gets or sets a value indicating whether the frame uses the extended specification /// @@ -48,7 +52,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the frame component collection /// - public Component[] Components { get; set; } + public FrameComponent[] Components { get; set; } /// /// Gets or sets the maximum horizontal sampling factor @@ -69,5 +73,36 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// Gets or sets the number of MCU's per column /// public int McusPerColumn { get; set; } + + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Whether to dispose of managed objects + protected virtual void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + foreach (FrameComponent component in this.Components) + { + component.Dispose(); + } + } + + // Set large fields to null. + this.Components = null; + + this.isDisposed = true; + } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs similarity index 75% rename from src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs rename to src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs index ca8744022a..18176bde73 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs @@ -1,20 +1,29 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; + + using ImageSharp.Memory; + /// - /// Represents a single color component + /// Represents a single frame component /// - internal struct Component + internal struct FrameComponent : IDisposable { /// /// Gets or sets the component Id /// public byte Id; + /// + /// TODO: What does pred stand for? + /// + public int Pred; + /// /// Gets or sets the horizontal sampling factor. /// @@ -38,7 +47,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the block data /// - public short[] BlockData; + public Buffer BlockData; /// /// Gets or sets the number of blocks per line @@ -59,5 +68,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// Gets the index for the AC Huffman table /// public int ACHuffmanTableId; + + /// + public void Dispose() + { + this.BlockData?.Dispose(); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs index 0f0a9b5408..d716355ad3 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs @@ -5,7 +5,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { - using System.Collections.Generic; + using System.Runtime.CompilerServices; /// /// Represents a branch in the huffman tree @@ -23,32 +23,31 @@ namespace ImageSharp.Formats.Jpeg.Port.Components public short Value; /// - /// The children + /// The children. /// - public List Children; + public HuffmanBranch[] Children; /// /// Initializes a new instance of the struct. /// /// The value + [MethodImpl(MethodImplOptions.AggressiveInlining)] public HuffmanBranch(short value) - : this(value, new List()) { + this.Index = 0; + this.Value = value; + this.Children = new HuffmanBranch[2]; } /// /// Initializes a new instance of the struct. /// /// The branch children - public HuffmanBranch(List children) - : this((short)0, children) - { - } - - private HuffmanBranch(short value, List children) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HuffmanBranch(HuffmanBranch[] children) { this.Index = 0; - this.Value = value; + this.Value = -1; this.Children = children; } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index 08c37bcbcc..a040d21e70 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -13,16 +13,16 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// internal class HuffmanTables { - private List first = new List(); + private HuffmanBranch[] first; - private List second = new List(); + private HuffmanBranch[] second; /// /// Gets or sets the table at the given index. /// /// The index /// The - public List this[int index] + public HuffmanBranch[] this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs index f808eecfde..1ca31b31bf 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs @@ -5,6 +5,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System.Runtime.CompilerServices; + using ImageSharp.Memory; /// @@ -16,24 +18,30 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets the ZigZag scan table /// - public static byte[] DctZigZag { get; } = + public static byte[] DctZigZag { - 0, - 1, 8, - 16, 9, 2, - 3, 10, 17, 24, - 32, 25, 18, 11, 4, - 5, 12, 19, 26, 33, 40, - 48, 41, 34, 27, 20, 13, 6, - 7, 14, 21, 28, 35, 42, 49, 56, - 57, 50, 43, 36, 29, 22, 15, - 23, 30, 37, 44, 51, 58, - 59, 52, 45, 38, 31, - 39, 46, 53, 60, - 61, 54, 47, - 55, 62, - 63 - }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + = + { + 0, + 1, 8, + 16, 9, 2, + 3, 10, 17, 24, + 32, 25, 18, 11, 4, + 5, 12, 19, 26, 33, 40, + 48, 41, 34, 27, 20, 13, 6, + 7, 14, 21, 28, 35, 42, 49, 56, + 57, 50, 43, 36, 29, 22, 15, + 23, 30, 37, 44, 51, 58, + 59, 52, 45, 38, 31, + 39, 46, 53, 60, + 61, 54, 47, + 55, 62, + 63 + }; /// /// Gets or sets the quantization tables. diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs new file mode 100644 index 0000000000..09837aef05 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -0,0 +1,471 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; +#if DEBUG + using System.Diagnostics; +#endif + using System.IO; + using System.Runtime.CompilerServices; + + /// + /// Encapsulates a decode method. TODO: This may well be a bottleneck + /// + /// The component + /// The offset + /// The DC Huffman tables + /// The AC Huffman tables + /// The input stream + internal delegate void DecodeAction(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream); + + /// + /// Provides the means to decode a spectral scan + /// + internal struct ScanDecoder + { + private int bitsData; + + private int bitsCount; + + private int specStart; + + private int specEnd; + + private int eobrun; + + private int successiveState; + + private int successiveACState; + + /// + /// Decodes the spectral scan + /// + /// The image frame + /// The input stream + /// The DC Huffman tables + /// The AC Huffman tables + /// The scan components + /// The reset interval + /// The spectral selection start + /// The spectral selection end + /// The successive approximation bit high end + /// The successive approximation bit low end + public void DecodeScan(Frame frame, Stream stream, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, FrameComponent[] components, ushort resetInterval, int spectralStart, int spectralEnd, int successivePrev, int successive) + { + this.specStart = spectralStart; + this.specEnd = spectralEnd; + this.successiveState = successive; + bool progressive = frame.Progressive; + int componentsLength = components.Length; + int mcusPerLine = frame.McusPerLine; + + // TODO: Delegate action will not be fast + DecodeAction decodeFn; + + if (progressive) + { + if (this.specStart == 0) + { + if (successivePrev == 0) + { + decodeFn = this.DecodeDCFirst; + } + else + { + decodeFn = this.DecodeDCSuccessive; + } + } + else + { + if (successivePrev == 0) + { + decodeFn = this.DecodeACFirst; + } + else + { + decodeFn = this.DecodeACSuccessive; + } + } + } + else + { + decodeFn = this.DecodeBaseline; + } + + int mcu = 0; + int mcuExpected; + if (componentsLength == 1) + { + mcuExpected = components[0].BlocksPerLine * components[0].BlocksPerColumn; + } + else + { + mcuExpected = mcusPerLine * frame.McusPerColumn; + } + + FileMarker fileMarker; + while (mcu < mcuExpected) + { + // Reset interval stuff + int mcuToRead = resetInterval > 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent c = ref components[i]; + c.Pred = 0; + } + + this.eobrun = 0; + + if (componentsLength == 1) + { + ref FrameComponent component = ref components[0]; + for (int n = 0; n < mcuToRead; n++) + { + DecodeBlock(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + DecodeMcu(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + + // Find marker + this.bitsCount = 0; + + // TODO: We need to make sure we are not overwriting anything here. + fileMarker = JpegDecoderCore.FindNextFileMarker(stream); + + // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); +#endif + } + + ushort marker = fileMarker.Marker; + if (marker <= 0xFF00) + { + throw new ImageFormatException("Marker was not found"); + } + + if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) + { + // RSTx + stream.Skip(2); + } + else + { + break; + } + } + + fileMarker = JpegDecoderCore.FindNextFileMarker(stream); + + // Some images include more Scan blocks than expected, skip past those and + // attempt to find the next valid marker (fixes issue8182.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockBufferOffset(FrameComponent component, int row, int col) + { + return 64 * (((component.BlocksPerLine + 1) * row) + col); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeMcu(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, DecodeAction decode, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = (mcu / mcusPerLine) | 0; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeBlock(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, DecodeAction decode, int mcu, Stream stream) + { + int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadBit(Stream stream) + { + if (this.bitsCount > 0) + { + this.bitsCount--; + return (this.bitsData >> this.bitsCount) & 1; + } + + this.bitsData = stream.ReadByte(); + if (this.bitsData == 0xFF) + { + int nextByte = stream.ReadByte(); + if (nextByte > 0) + { + throw new ImageFormatException($"Unexpected marker {(this.bitsData << 8) | nextByte}"); + } + + // Unstuff 0 + } + + this.bitsCount = 7; + return this.bitsData >> 7; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private short DecodeHuffman(HuffmanBranch[] tree, Stream stream) + { + HuffmanBranch[] node = tree; + while (true) + { + int index; + index = this.ReadBit(stream); + HuffmanBranch branch = node[index]; + node = branch.Children; + + if (branch.Value > -1) + { + return branch.Value; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int Receive(int length, Stream stream) + { + int n = 0; + while (length > 0) + { + n = (n << 1) | this.ReadBit(stream); + length--; + } + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReceiveAndExtend(int length, Stream stream) + { + if (length == 1) + { + return this.ReadBit(stream) == 1 ? 1 : -1; + } + + int n = this.Receive(length, stream); + if (n >= 1 << (length - 1)) + { + return n; + } + + return n + (-1 << length) + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBaseline(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream); + component.BlockData[offset] = (short)(component.Pred += diff); + + int k = 1; + while (k < 64) + { + int rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int s = rs & 15; + int r = rs >> 4; + + if (s == 0) + { + if (r < 15) + { + break; + } + + k += 16; + continue; + } + + k += r; + byte z = QuantizationTables.DctZigZag[k]; + short re = (short)this.ReceiveAndExtend(s, stream); + component.BlockData[offset + z] = re; + k++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream) << this.successiveState; + component.BlockData[offset] = (short)(component.Pred += diff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeDCSuccessive(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + component.BlockData[offset] |= (short)(this.ReadBit(stream) << this.successiveState); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeACFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + if (this.eobrun > 0) + { + this.eobrun--; + return; + } + + int k = this.specStart; + int e = this.specEnd; + while (k <= e) + { + short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int s = rs & 15; + int r = rs >> 4; + + if (s == 0) + { + if (r < 15) + { + this.eobrun = this.Receive(r, stream) + (1 << r) - 1; + break; + } + + k += 16; + continue; + } + + k += r; + byte z = QuantizationTables.DctZigZag[k]; + component.BlockData[offset + z] = (short)(this.ReceiveAndExtend(s, stream) * (1 << this.successiveState)); + k++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeACSuccessive(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + int k = this.specStart; + int e = this.specEnd; + int r = 0; + while (k <= e) + { + byte z = QuantizationTables.DctZigZag[k]; + int successiveACNextValue = 0; + switch (this.successiveACState) + { + case 0: // Initial state + short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int s = rs & 15; + r = rs >> 4; + if (s == 0) + { + if (r < 15) + { + this.eobrun = this.Receive(r, stream) + (1 << r); + this.successiveACState = 4; + } + else + { + r = 16; + this.successiveACState = 1; + } + } + else + { + if (s != 1) + { + throw new ImageFormatException("Invalid ACn encoding"); + } + + successiveACNextValue = this.ReceiveAndExtend(s, stream); + this.successiveACState = r > 0 ? 2 : 3; + } + + continue; + case 1: // Skipping r zero items + case 2: + if (component.BlockData[offset + z] > 0) + { + component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + } + else + { + r--; + if (r == 0) + { + this.successiveACState = this.successiveACState == 2 ? 3 : 0; + } + } + + break; + case 3: // Set value for a zero item + if (component.BlockData[offset + z] > 0) + { + component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + } + else + { + component.BlockData[offset + z] = (short)(successiveACNextValue << this.successiveState); + this.successiveACState = 0; + } + + break; + case 4: // Eob + if (component.BlockData[offset + z] > 0) + { + component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + } + + break; + } + + k++; + } + + if (this.successiveACState == 4) + { + this.eobrun--; + if (this.eobrun == 0) + { + this.successiveACState = 0; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs index f26dbded5d..0ad8afa911 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -173,6 +173,24 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const ushort SOS = 0xFFDA; + /// + /// Define First Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const ushort RST0 = 0xFFD0; + + /// + /// Define Eigth Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const ushort RST7 = 0xFFD7; + /// /// Contains JFIF specific markers /// diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 9dd45f53b1..873d4623db 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -8,6 +8,7 @@ namespace ImageSharp.Formats.Jpeg.Port using System; using System.Collections.Generic; using System.IO; + using System.Runtime.CompilerServices; using ImageSharp.Common.Extensions; using ImageSharp.Formats.Jpeg.Port.Components; @@ -18,7 +19,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// Performs the jpeg decoding operation. /// Ported from /// - internal class JpegDecoderCore + internal class JpegDecoderCore : IDisposable { /// /// The decoder options. @@ -48,10 +49,12 @@ namespace ImageSharp.Formats.Jpeg.Port private ushort resetInterval; /// - /// COntains information about the jFIF marker + /// Contains information about the jFIF marker /// private JFif jFif; + private bool isDisposed; + /// /// Initializes a new instance of the class. /// @@ -68,6 +71,109 @@ namespace ImageSharp.Formats.Jpeg.Port /// public Stream InputStream { get; private set; } + /// + /// Finds the next file marker within the byte stream. Not used but I'm keeping it for now for testing + /// + /// The input stream + /// The + public static FileMarker FindNextFileMarkerOld(Stream stream) + { + byte[] buffer = new byte[2]; + while (true) + { + int value = stream.Read(buffer, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + while (buffer[0] != JpegConstants.Markers.Prefix) + { + // Strictly speaking, this is a format error. However, libjpeg is + // liberal in what it accepts. As of version 9, next_marker in + // jdmarker.c treats this as a warning (JWRN_EXTRANEOUS_DATA) and + // continues to decode the stream. Even before next_marker sees + // extraneous data, jpeg_fill_bit_buffer in jdhuff.c reads as many + // bytes as it can, possibly past the end of a scan's data. It + // effectively puts back any markers that it overscanned (e.g. an + // "\xff\xd9" EOI marker), but it does not put back non-marker data, + // and thus it can silently ignore a small number of extraneous + // non-marker bytes before next_marker has a chance to see them (and + // print a warning). + // We are therefore also liberal in what we accept. Extraneous data + // is silently ignore + // This is similar to, but not exactly the same as, the restart + // mechanism within a scan (the RST[0-7] markers). + // Note that extraneous 0xff bytes in e.g. SOS data are escaped as + // "\xff\x00", and so are detected a little further down below. + buffer[0] = buffer[1]; + + value = stream.ReadByte(); + if (value == -1) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + buffer[1] = (byte)value; + } + + return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); + } + } + + /// + /// Finds the next file marker within the byte stream + /// + /// The input stream + /// The + public static FileMarker FindNextFileMarker(Stream stream) + { + byte[] buffer = new byte[2]; + long maxPos = stream.Length - 1; + long currentPos = stream.Position; + long newPos = currentPos; + + if (currentPos >= maxPos) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + int value = stream.Read(buffer, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + ushort currentMarker = (ushort)((buffer[0] << 8) | buffer[1]); + if (currentMarker >= JpegConstants.Markers.SOF0 && currentMarker <= JpegConstants.Markers.COM) + { + return new FileMarker(currentMarker, stream.Position - 2); + } + + value = stream.Read(buffer, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + ushort newMarker = (ushort)((buffer[0] << 8) | buffer[1]); + while (!(newMarker >= JpegConstants.Markers.SOF0 && newMarker <= JpegConstants.Markers.COM)) + { + if (++newPos >= maxPos) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + stream.Read(buffer, 0, 2); + newMarker = (ushort)((buffer[0] << 8) | buffer[1]); + } + + return new FileMarker(newMarker, newPos, true); + } + /// /// Decodes the image from the specified and sets /// the data to image. @@ -85,27 +191,52 @@ namespace ImageSharp.Formats.Jpeg.Port return image; } + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Whether to dispose of managed objects + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.frame.Dispose(); + } + + // TODO: set large fields to null. + this.isDisposed = true; + } + } + private void ParseStream() { // Check for the Start Of Image marker. - ushort fileMarker = this.ReadUint16(); - if (fileMarker != JpegConstants.Markers.SOI) + var fileMarker = new FileMarker(this.ReadUint16(), 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) { throw new ImageFormatException("Missing SOI marker."); } - fileMarker = this.ReadUint16(); + ushort marker = this.ReadUint16(); + fileMarker = new FileMarker(marker, (int)this.InputStream.Position - 2); this.quantizationTables = new QuantizationTables(); this.dcHuffmanTables = new HuffmanTables(); this.acHuffmanTables = new HuffmanTables(); - while (fileMarker != JpegConstants.Markers.EOI) + while (fileMarker.Marker != JpegConstants.Markers.EOI) { // Get the marker length int remaining = this.ReadUint16() - 2; - switch (fileMarker) + switch (fileMarker.Marker) { case JpegConstants.Markers.APP0: this.ProcessApplicationHeaderMarker(remaining); @@ -148,12 +279,12 @@ namespace ImageSharp.Formats.Jpeg.Port break; case JpegConstants.Markers.SOS: - this.ProcessStartOfScan(); + this.ProcessStartOfScanMarker(); break; } // Read on - fileMarker = this.FindNextFileMarker(); + fileMarker = FindNextFileMarker(this.InputStream); } } @@ -281,7 +412,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// The remaining bytes in the segment block. /// The current frame marker. - private void ProcessStartOfFrameMarker(int remaining, ushort frameMarker) + private void ProcessStartOfFrameMarker(int remaining, FileMarker frameMarker) { if (this.frame != null) { @@ -292,8 +423,8 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame = new Frame { - Extended = frameMarker == JpegConstants.Markers.SOF1, - Progressive = frameMarker == JpegConstants.Markers.SOF2, + Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, + Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2, Precision = this.temp[0], Scanlines = (short)((this.temp[1] << 8) | this.temp[2]), SamplesPerLine = (short)((this.temp[3] << 8) | this.temp[4]), @@ -304,9 +435,9 @@ namespace ImageSharp.Formats.Jpeg.Port int maxV = 0; int index = 6; - // TODO: Pool this. + // No need to pool this. They max out at 4 this.frame.ComponentIds = new byte[this.frame.ComponentCount]; - this.frame.Components = new Component[this.frame.ComponentCount]; + this.frame.Components = new FrameComponent[this.frame.ComponentCount]; for (int i = 0; i < this.frame.ComponentCount; i++) { @@ -389,30 +520,36 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Processes the SOS (Start of scan marker). /// - private void ProcessStartOfScan() + private void ProcessStartOfScanMarker() { int selectorsCount = this.InputStream.ReadByte(); - var components = new List(); - for (int i = 0; i < selectorsCount; i++) { byte componentIndex = this.frame.ComponentIds[this.InputStream.ReadByte() - 1]; - Component component = this.frame.Components[componentIndex]; + ref FrameComponent component = ref this.frame.Components[componentIndex]; int tableSpec = this.InputStream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; - components.Add(component); } this.InputStream.Read(this.temp, 0, 3); + int spectralStart = this.temp[0]; int spectralEnd = this.temp[1]; int successiveApproximation = this.temp[2]; - } - - private int DecodeScan(List components, int spectralStart, int spectralEnd, int successivePrev, int successive) - { - return 0; + var scanDecoder = default(ScanDecoder); + + scanDecoder.DecodeScan( + this.frame, + this.InputStream, + this.dcHuffmanTables, + this.acHuffmanTables, + this.frame.Components, + this.resetInterval, + spectralStart, + spectralEnd, + successiveApproximation >> 4, + successiveApproximation & 15); } /// @@ -430,8 +567,8 @@ namespace ImageSharp.Formats.Jpeg.Port length--; } - // TODO: Check the capacity here. Seems to max at 2 - var code = new List { new HuffmanBranch(new List()) }; + // TODO: Check the branch children capacity here. Seems to max at 2 + var code = new List { new HuffmanBranch(-1) }; HuffmanBranch p = code[0]; int k = 0; @@ -441,7 +578,7 @@ namespace ImageSharp.Formats.Jpeg.Port for (int j = 0; j < codeLengths[i]; j++) { p = code.Pop(); - p.Children.SafeInsert(p.Index, new HuffmanBranch(values[k])); + p.Children[p.Index] = new HuffmanBranch(values[k]); while (p.Index > 0) { p = code.Pop(); @@ -451,9 +588,9 @@ namespace ImageSharp.Formats.Jpeg.Port code.Add(p); while (code.Count <= i) { - q = new HuffmanBranch(new List()); + q = new HuffmanBranch(-1); code.Add(q); - p.Children.SafeInsert(p.Index, new HuffmanBranch(q.Children)); + p.Children[p.Index] = new HuffmanBranch(q.Children); p = q; } @@ -463,9 +600,9 @@ namespace ImageSharp.Formats.Jpeg.Port if (i + 1 < length) { // p here points to last code - q = new HuffmanBranch(new List()); + q = new HuffmanBranch(-1); code.Add(q); - p.Children.SafeInsert(p.Index, new HuffmanBranch(q.Children)); + p.Children[p.Index] = new HuffmanBranch(q.Children); p = q; } } @@ -478,21 +615,21 @@ namespace ImageSharp.Formats.Jpeg.Port /// private void PrepareComponents() { - int mcusPerLine = this.frame.SamplesPerLine / 8 / this.frame.MaxHorizontalFactor; - int mcusPerColumn = this.frame.Scanlines / 8 / this.frame.MaxVerticalFactor; + int mcusPerLine = (int)Math.Ceiling(this.frame.SamplesPerLine / 8D / this.frame.MaxHorizontalFactor); + int mcusPerColumn = (int)Math.Ceiling(this.frame.Scanlines / 8D / this.frame.MaxVerticalFactor); for (int i = 0; i < this.frame.ComponentCount; i++) { ref var component = ref this.frame.Components[i]; - int blocksPerLine = this.frame.SamplesPerLine / 8 * component.HorizontalFactor / this.frame.MaxHorizontalFactor; - int blocksPerColumn = this.frame.Scanlines / 8 * component.VerticalFactor / this.frame.MaxVerticalFactor; + int blocksPerLine = (int)Math.Ceiling(Math.Ceiling(this.frame.SamplesPerLine / 8D) * component.HorizontalFactor / this.frame.MaxHorizontalFactor); + int blocksPerColumn = (int)Math.Ceiling(Math.Ceiling(this.frame.Scanlines / 8D) * component.VerticalFactor / this.frame.MaxVerticalFactor); int blocksPerLineForMcu = mcusPerLine * component.HorizontalFactor; int blocksPerColumnForMcu = mcusPerColumn * component.VerticalFactor; int blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1); - // TODO: Pool this - component.BlockData = new short[blocksBufferSize]; + // Pooled. Disposed via frame siposal + component.BlockData = new Buffer(blocksBufferSize); component.BlocksPerLine = blocksPerLine; component.BlocksPerColumn = blocksPerColumn; } @@ -501,59 +638,11 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame.McusPerColumn = mcusPerColumn; } - /// - /// Finds the next file marker within the byte stream - /// - /// The - private ushort FindNextFileMarker() - { - while (true) - { - int value = this.InputStream.Read(this.uint16Buffer, 0, 2); - - if (value == 0) - { - return JpegConstants.Markers.EOI; - } - - while (this.uint16Buffer[0] != JpegConstants.Markers.Prefix) - { - // Strictly speaking, this is a format error. However, libjpeg is - // liberal in what it accepts. As of version 9, next_marker in - // jdmarker.c treats this as a warning (JWRN_EXTRANEOUS_DATA) and - // continues to decode the stream. Even before next_marker sees - // extraneous data, jpeg_fill_bit_buffer in jdhuff.c reads as many - // bytes as it can, possibly past the end of a scan's data. It - // effectively puts back any markers that it overscanned (e.g. an - // "\xff\xd9" EOI marker), but it does not put back non-marker data, - // and thus it can silently ignore a small number of extraneous - // non-marker bytes before next_marker has a chance to see them (and - // print a warning). - // We are therefore also liberal in what we accept. Extraneous data - // is silently ignore - // This is similar to, but not exactly the same as, the restart - // mechanism within a scan (the RST[0-7] markers). - // 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.uint16Buffer[0] = this.uint16Buffer[1]; - - value = this.InputStream.ReadByte(); - if (value == -1) - { - return JpegConstants.Markers.EOI; - } - - this.uint16Buffer[1] = (byte)value; - } - - return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); - } - } - /// /// Reads a from the stream advancing it by two bytes /// /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] private ushort ReadUint16() { this.InputStream.Read(this.uint16Buffer, 0, 2); diff --git a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs index 455af48ad2..56771bcf58 100644 --- a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs @@ -23,21 +23,21 @@ namespace ImageSharp.Benchmarks.Image { if (this.jpegBytes == null) { - this.jpegBytes = File.ReadAllBytes("../ImageSharp.Tests/TestImages/Formats/Jpg/Baseline/Calliphora.jpg"); + this.jpegBytes = File.ReadAllBytes("../../../../../../../../ImageSharp.Tests/TestImages/Formats/Jpg/Baseline/Calliphora.jpg"); } } - [Benchmark(Baseline = true, Description = "System.Drawing Jpeg")] - public Size JpegSystemDrawing() - { - using (MemoryStream memoryStream = new MemoryStream(this.jpegBytes)) - { - using (Image image = Image.FromStream(memoryStream)) - { - return image.Size; - } - } - } + //[Benchmark(Baseline = true, Description = "System.Drawing Jpeg")] + //public Size JpegSystemDrawing() + //{ + // using (MemoryStream memoryStream = new MemoryStream(this.jpegBytes)) + // { + // using (Image image = Image.FromStream(memoryStream)) + // { + // return image.Size; + // } + // } + //} [Benchmark(Description = "ImageSharp Jpeg")] public CoreSize JpegCore()