diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs index 8445625bd..e2b72db05 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs @@ -57,8 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder /// /// The to use for buffer allocations. /// The instance - /// Whether to decode metadata only. If this is true, memory allocation for SpectralBlocks will not be necessary - public void InitializeDerivedData(MemoryManager memoryManager, OrigJpegDecoderCore decoder, bool metadataOnly) + public void InitializeDerivedData(MemoryManager memoryManager, OrigJpegDecoderCore decoder) { // For 4-component images (either CMYK or YCbCrK), we only support two // hv vectors: [0x11 0x11 0x11 0x11] and [0x22 0x11 0x11 0x22]. @@ -81,10 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder this.SubSamplingDivisors = c0.SamplingFactors.DivideBy(this.SamplingFactors); } - if (!metadataOnly) - { - this.SpectralBlocks = memoryManager.Allocate2D(this.SizeInBlocks.Width, this.SizeInBlocks.Height, true); - } + this.SpectralBlocks = memoryManager.Allocate2D(this.SizeInBlocks.Width, this.SizeInBlocks.Height, true); } /// diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index 66b4601da..875f16ec2 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -87,8 +87,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort { this.IgnoreMetadata = options.IgnoreMetadata; this.configuration = configuration ?? Configuration.Default; - this.HuffmanTrees = OrigHuffmanTree.CreateHuffmanTrees(); - this.QuantizationTables = new Block8x8F[MaxTq + 1]; this.Temp = new byte[2 * Block8x8F.Size]; } @@ -103,10 +101,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort /// /// Gets the huffman trees /// - public OrigHuffmanTree[] HuffmanTrees { get; } + public OrigHuffmanTree[] HuffmanTrees { get; private set; } /// - public Block8x8F[] QuantizationTables { get; } + public Block8x8F[] QuantizationTables { get; private set; } /// /// Gets the temporary buffer used to store bytes read from the stream. @@ -193,7 +191,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort where TPixel : struct, IPixel { this.ParseStream(stream); - return this.PostProcessIntoImage(); } @@ -204,7 +201,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort public IImageInfo Identify(Stream stream) { this.ParseStream(stream, true); - return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); } @@ -215,7 +211,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort { foreach (OrigComponent component in this.Components) { - component.Dispose(); + component?.Dispose(); } } @@ -233,6 +229,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort this.InputStream = stream; this.InputProcessor = new InputProcessor(stream, this.Temp); + if (!metadataOnly) + { + this.HuffmanTrees = OrigHuffmanTree.CreateHuffmanTrees(); + this.QuantizationTables = new Block8x8F[MaxTq + 1]; + } + // Check for the Start Of Image marker. this.InputProcessor.ReadFull(this.Temp, 0, 2); @@ -332,10 +334,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort case OrigJpegConstants.Markers.SOF2: this.IsProgressive = marker == OrigJpegConstants.Markers.SOF2; this.ProcessStartOfFrameMarker(remaining, metadataOnly); - if (metadataOnly && this.isJFif) - { - return; - } break; case OrigJpegConstants.Markers.DHT: @@ -361,19 +359,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort break; case OrigJpegConstants.Markers.SOS: - if (metadataOnly) + if (!metadataOnly) { - return; + this.ProcessStartOfScanMarker(remaining); + if (this.InputProcessor.ReachedEOF) + { + // If unexpected EOF reached. We can stop processing bytes as we now have the image data. + processBytes = false; + } } - - this.ProcessStartOfScanMarker(remaining); - if (this.InputProcessor.ReachedEOF) + else { - // If unexpected EOF reached. We can stop processing bytes as we now have the image data. + // It's highly unlikely that APPn related data will be found after the SOS marker + // We should have gathered everything we need by now. processBytes = false; } break; + case OrigJpegConstants.Markers.DRI: if (metadataOnly) { @@ -425,7 +428,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort /// private void InitDerivedMetaDataProperties() { - if (this.isExif) + if (this.isJFif) + { + this.MetaData.HorizontalResolution = this.jFif.XDensity; + this.MetaData.VerticalResolution = this.jFif.YDensity; + } + else if (this.isExif) { double horizontalValue = this.MetaData.ExifProfile.TryGetValue(ExifTag.XResolution, out ExifValue horizonalTag) ? ((Rational)horizonalTag.Value).ToDouble() @@ -441,11 +449,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort this.MetaData.VerticalResolution = verticalValue; } } - else if (this.isJFif) - { - this.MetaData.HorizontalResolution = this.jFif.XDensity; - this.MetaData.VerticalResolution = this.jFif.YDensity; - } } /// @@ -675,26 +678,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort throw new ImageFormatException("SOF has wrong length"); } - this.Components = new OrigComponent[this.ComponentCount]; - - for (int i = 0; i < this.ComponentCount; i++) + if (!metadataOnly) { - byte componentIdentifier = this.Temp[6 + (3 * i)]; - var component = new OrigComponent(componentIdentifier, i); - component.InitializeCoreData(this); - this.Components[i] = component; - } + this.Components = new OrigComponent[this.ComponentCount]; - int h0 = this.Components[0].HorizontalSamplingFactor; - int v0 = this.Components[0].VerticalSamplingFactor; + for (int i = 0; i < this.ComponentCount; i++) + { + byte componentIdentifier = this.Temp[6 + (3 * i)]; + var component = new OrigComponent(componentIdentifier, i); + component.InitializeCoreData(this); + this.Components[i] = component; + } - this.ImageSizeInMCU = this.ImageSizeInPixels.DivideRoundUp(8 * h0, 8 * v0); + int h0 = this.Components[0].HorizontalSamplingFactor; + int v0 = this.Components[0].VerticalSamplingFactor; - this.ColorSpace = this.DeduceJpegColorSpace(); + this.ImageSizeInMCU = this.ImageSizeInPixels.DivideRoundUp(8 * h0, 8 * v0); - foreach (OrigComponent component in this.Components) - { - component.InitializeDerivedData(this.configuration.MemoryManager, this, metadataOnly); + this.ColorSpace = this.DeduceJpegColorSpace(); + + foreach (OrigComponent component in this.Components) + { + component.InitializeDerivedData(this.configuration.MemoryManager, this); + } } } diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs new file mode 100644 index 000000000..eb91590e8 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs @@ -0,0 +1,238 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Memory; + +// TODO: This could be useful elsewhere. +namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components +{ + /// + /// A stream reader that add a secondary level buffer in addition to native stream buffered reading + /// to reduce the overhead of small incremental reads. + /// + internal class DoubleBufferedStreamReader : IDisposable + { + /// + /// The length, in bytes, of the buffering chunk + /// + public const int ChunkLength = 4096; + + private const int ChunkLengthMinusOne = ChunkLength - 1; + + private readonly Stream stream; + + private readonly IManagedByteBuffer managedBuffer; + + private readonly byte[] bufferChunk; + + private readonly int length; + + private int bytesRead; + + private int position; + + /// + /// Initializes a new instance of the class. + /// + /// The to use for buffer allocations. + /// The input stream. + public DoubleBufferedStreamReader(MemoryManager memoryManager, Stream stream) + { + this.stream = stream; + this.length = (int)stream.Length; + this.managedBuffer = memoryManager.AllocateCleanManagedByteBuffer(ChunkLength); + this.bufferChunk = this.managedBuffer.Array; + } + + /// + /// Gets the length, in bytes, of the stream + /// + public long Length => this.length; + + /// + /// Gets or sets the current position within the stream + /// + public long Position + { + get => this.position; + + set + { + // Reset everything. It's easier than tracking. + this.position = (int)value; + this.stream.Seek(this.position, SeekOrigin.Begin); + this.bytesRead = ChunkLength; + } + } + + /// + /// Reads a byte from the stream and advances the position within the stream by one + /// byte, or returns -1 if at the end of the stream. + /// + /// The unsigned byte cast to an , or -1 if at the end of the stream. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadByte() + { + if (this.position >= this.length) + { + return -1; + } + + if (this.position == 0 || this.bytesRead > ChunkLengthMinusOne) + { + return this.ReadByteSlow(); + } + + this.position++; + return this.bufferChunk[this.bytesRead++]; + } + + /// + /// Skips the number of bytes in the stream + /// + /// The number of bytes to skip + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Skip(int count) + { + this.Position += count; + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream + /// by the number of bytes read. + /// + /// + /// An array of bytes. When this method returns, the buffer contains the specified + /// byte array with the values between offset and (offset + count - 1) replaced by + /// the bytes read from the current source. + /// + /// + /// The zero-based byte offset in buffer at which to begin storing the data read + /// from the current stream. + /// + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number + /// of bytes requested if that many bytes are not currently available, or zero (0) + /// if the end of the stream has been reached. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Read(byte[] buffer, int offset, int count) + { + if (buffer.Length > ChunkLength) + { + return this.ReadToBufferSlow(buffer, offset, count); + } + + if (this.position == 0 || count + this.bytesRead > ChunkLength) + { + return this.ReadToChunkSlow(buffer, offset, count); + } + + int n = this.GetCount(count); + this.CopyBytes(buffer, offset, n); + + this.position += n; + this.bytesRead += n; + + return n; + } + + /// + public void Dispose() + { + this.managedBuffer?.Dispose(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadByteSlow() + { + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + this.stream.Read(this.bufferChunk, 0, ChunkLength); + this.bytesRead = 0; + + this.position++; + return this.bufferChunk[this.bytesRead++]; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToChunkSlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + this.stream.Read(this.bufferChunk, 0, ChunkLength); + this.bytesRead = 0; + + int n = this.GetCount(count); + this.CopyBytes(buffer, offset, n); + + this.position += n; + this.bytesRead += n; + + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferSlow(byte[] buffer, int offset, int count) + { + // Read to target but don't copy to our chunk. + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + int n = this.stream.Read(buffer, offset, count); + this.Position += n; + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetCount(int count) + { + int n = this.length - this.position; + if (n > count) + { + n = count; + } + + if (n < 0) + { + n = 0; + } + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyBytes(byte[] buffer, int offset, int count) + { + if (count < 9) + { + int byteCount = count; + int read = this.bytesRead; + byte[] chunk = this.bufferChunk; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = chunk[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.bufferChunk, this.bytesRead, buffer, offset, count); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsFileMarker.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsFileMarker.cs index 8e51c0b7c..85c9f9466 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsFileMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsFileMarker.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components { /// @@ -13,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// /// The marker /// The position within the stream - public PdfJsFileMarker(ushort marker, long position) + public PdfJsFileMarker(byte marker, long position) { this.Marker = marker; this.Position = position; @@ -26,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// The marker /// The position within the stream /// Whether the current marker is invalid - public PdfJsFileMarker(ushort marker, long position, bool invalid) + public PdfJsFileMarker(byte marker, long position, bool invalid) { this.Marker = marker; this.Position = position; @@ -36,17 +38,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// /// Gets a value indicating whether the current marker is invalid /// - public bool Invalid { get; } + public bool Invalid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } /// /// Gets the position of the marker within a stream /// - public ushort Marker { get; } + public byte Marker + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } /// /// Gets the position of the marker within a stream /// - public long Position { get; } + public long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } /// public override string ToString() diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsScanDecoder.cs index 5fcaa6cea..c6b14d6fb 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/PdfJsScanDecoder.cs @@ -5,7 +5,6 @@ using System; #if DEBUG using System.Diagnostics; #endif -using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jpeg.Common; @@ -21,6 +20,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components private byte[] markerBuffer; + private int mcuToRead; + + private int mcusPerLine; + + private int mcu; + private int bitsData; private int bitsCount; @@ -60,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// The successive approximation bit low end public void DecodeScan( PdfJsFrame frame, - Stream stream, + DoubleBufferedStreamReader stream, PdfJsHuffmanTables dcHuffmanTables, PdfJsHuffmanTables acHuffmanTables, PdfJsFrameComponent[] components, @@ -82,9 +87,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components this.unexpectedMarkerReached = false; bool progressive = frame.Progressive; - int mcusPerLine = frame.McusPerLine; + this.mcusPerLine = frame.McusPerLine; - int mcu = 0; + this.mcu = 0; int mcuExpected; if (componentsLength == 1) { @@ -92,14 +97,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components } else { - mcuExpected = mcusPerLine * frame.McusPerColumn; + mcuExpected = this.mcusPerLine * frame.McusPerColumn; } - PdfJsFileMarker fileMarker; - while (mcu < mcuExpected) + while (this.mcu < mcuExpected) { // Reset interval stuff - int mcuToRead = resetInterval != 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; + this.mcuToRead = resetInterval != 0 ? Math.Min(mcuExpected - this.mcu, resetInterval) : mcuExpected; for (int i = 0; i < components.Length; i++) { PdfJsFrameComponent c = components[i]; @@ -110,30 +114,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components if (!progressive) { - this.DecodeScanBaseline(dcHuffmanTables, acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + this.DecodeScanBaseline(dcHuffmanTables, acHuffmanTables, components, componentsLength, stream); } else { bool isAc = this.specStart != 0; bool isFirst = successivePrev == 0; PdfJsHuffmanTables huffmanTables = isAc ? acHuffmanTables : dcHuffmanTables; - this.DecodeScanProgressive(huffmanTables, isAc, isFirst, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + this.DecodeScanProgressive(huffmanTables, isAc, isFirst, components, componentsLength, stream); } - // Find marker + // Reset + // TODO: I do not understand why these values are reset? We should surely be tracking the bits across mcu's? this.bitsCount = 0; - fileMarker = PdfJsJpegDecoderCore.FindNextFileMarker(this.markerBuffer, 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 at {stream.Position}, next marker is: {fileMarker.Marker:X}"); -#endif - } + this.bitsData = 0; + this.unexpectedMarkerReached = false; - ushort marker = fileMarker.Marker; + // Some images include more scan blocks than expected, skip past those and + // attempt to find the next valid marker + PdfJsFileMarker fileMarker = PdfJsJpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); + byte marker = fileMarker.Marker; // RSTn - We've already read the bytes and altered the position so no need to skip if (marker >= PdfJsJpegConstants.Markers.RST0 && marker <= PdfJsJpegConstants.Markers.RST7) @@ -148,24 +148,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components stream.Position = fileMarker.Position; break; } - } - fileMarker = PdfJsJpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); - - // Some images include more Scan blocks than expected, skip past those and - // attempt to find the next valid marker (fixes issue8182.pdf) ref original code. - if (fileMarker.Invalid) - { #if DEBUG Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker:X}"); #endif } - else - { - // We've found a valid marker. - // Rewind the stream to the position of the marker - stream.Position = fileMarker.Position; - } } private void DecodeScanBaseline( @@ -173,10 +160,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components PdfJsHuffmanTables acHuffmanTables, PdfJsFrameComponent[] components, int componentsLength, - int mcusPerLine, - int mcuToRead, - ref int mcu, - Stream stream) + DoubleBufferedStreamReader stream) { if (componentsLength == 1) { @@ -185,20 +169,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components ref PdfJsHuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; ref PdfJsHuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; - for (int n = 0; n < mcuToRead; n++) + for (int n = 0; n < this.mcuToRead; n++) { if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } - this.DecodeBlockBaseline(ref dcHuffmanTable, ref acHuffmanTable, component, ref blockDataRef, mcu, stream); - mcu++; + this.DecodeBlockBaseline(ref dcHuffmanTable, ref acHuffmanTable, component, ref blockDataRef, stream); + this.mcu++; } } else { - for (int n = 0; n < mcuToRead; n++) + for (int n = 0; n < this.mcuToRead; n++) { for (int i = 0; i < componentsLength; i++) { @@ -218,12 +202,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components continue; } - this.DecodeMcuBaseline(ref dcHuffmanTable, ref acHuffmanTable, component, ref blockDataRef, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuBaseline(ref dcHuffmanTable, ref acHuffmanTable, component, ref blockDataRef, j, k, stream); } } } - mcu++; + this.mcu++; } } } @@ -234,10 +218,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components bool isFirst, PdfJsFrameComponent[] components, int componentsLength, - int mcusPerLine, - int mcuToRead, - ref int mcu, - Stream stream) + DoubleBufferedStreamReader stream) { if (componentsLength == 1) { @@ -245,7 +226,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components ref short blockDataRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(component.SpectralBlocks.Span)); ref PdfJsHuffmanTable huffmanTable = ref huffmanTables[isAC ? component.ACHuffmanTableId : component.DCHuffmanTableId]; - for (int n = 0; n < mcuToRead; n++) + for (int n = 0; n < this.mcuToRead; n++) { if (this.endOfStreamReached || this.unexpectedMarkerReached) { @@ -256,31 +237,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components { if (isFirst) { - this.DecodeBlockACFirst(ref huffmanTable, component, ref blockDataRef, mcu, stream); + this.DecodeBlockACFirst(ref huffmanTable, component, ref blockDataRef, stream); } else { - this.DecodeBlockACSuccessive(ref huffmanTable, component, ref blockDataRef, mcu, stream); + this.DecodeBlockACSuccessive(ref huffmanTable, component, ref blockDataRef, stream); } } else { if (isFirst) { - this.DecodeBlockDCFirst(ref huffmanTable, component, ref blockDataRef, mcu, stream); + this.DecodeBlockDCFirst(ref huffmanTable, component, ref blockDataRef, stream); } else { - this.DecodeBlockDCSuccessive(component, ref blockDataRef, mcu, stream); + this.DecodeBlockDCSuccessive(component, ref blockDataRef, stream); } } - mcu++; + this.mcu++; } } else { - for (int n = 0; n < mcuToRead; n++) + for (int n = 0; n < this.mcuToRead; n++) { for (int i = 0; i < componentsLength; i++) { @@ -294,56 +275,57 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components { for (int k = 0; k < h; k++) { + // No need to continue here. if (this.endOfStreamReached || this.unexpectedMarkerReached) { - continue; + break; } if (isAC) { if (isFirst) { - this.DecodeMcuACFirst(ref huffmanTable, component, ref blockDataRef, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuACFirst(ref huffmanTable, component, ref blockDataRef, j, k, stream); } else { - this.DecodeMcuACSuccessive(ref huffmanTable, component, ref blockDataRef, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuACSuccessive(ref huffmanTable, component, ref blockDataRef, j, k, stream); } } else { if (isFirst) { - this.DecodeMcuDCFirst(ref huffmanTable, component, ref blockDataRef, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuDCFirst(ref huffmanTable, component, ref blockDataRef, j, k, stream); } else { - this.DecodeMcuDCSuccessive(component, ref blockDataRef, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuDCSuccessive(component, ref blockDataRef, j, k, stream); } } } } } - mcu++; + this.mcu++; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockBaseline(ref PdfJsHuffmanTable dcHuffmanTable, ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcu, Stream stream) + private void DecodeBlockBaseline(ref PdfJsHuffmanTable dcHuffmanTable, ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, DoubleBufferedStreamReader stream) { - int blockRow = mcu / component.WidthInBlocks; - int blockCol = mcu % component.WidthInBlocks; + int blockRow = this.mcu / component.WidthInBlocks; + int blockCol = this.mcu % component.WidthInBlocks; int offset = component.GetBlockBufferOffset(blockRow, blockCol); this.DecodeBaseline(component, ref blockDataRef, offset, ref dcHuffmanTable, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuBaseline(ref PdfJsHuffmanTable dcHuffmanTable, ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuBaseline(ref PdfJsHuffmanTable dcHuffmanTable, ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int row, int col, DoubleBufferedStreamReader stream) { - int mcuRow = mcu / mcusPerLine; - int mcuCol = mcu % mcusPerLine; + int mcuRow = this.mcu / this.mcusPerLine; + int mcuCol = this.mcu % this.mcusPerLine; int blockRow = (mcuRow * component.VerticalSamplingFactor) + row; int blockCol = (mcuCol * component.HorizontalSamplingFactor) + col; int offset = component.GetBlockBufferOffset(blockRow, blockCol); @@ -351,19 +333,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockDCFirst(ref PdfJsHuffmanTable dcHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcu, Stream stream) + private void DecodeBlockDCFirst(ref PdfJsHuffmanTable dcHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, DoubleBufferedStreamReader stream) { - int blockRow = mcu / component.WidthInBlocks; - int blockCol = mcu % component.WidthInBlocks; + int blockRow = this.mcu / component.WidthInBlocks; + int blockCol = this.mcu % component.WidthInBlocks; int offset = component.GetBlockBufferOffset(blockRow, blockCol); this.DecodeDCFirst(component, ref blockDataRef, offset, ref dcHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuDCFirst(ref PdfJsHuffmanTable dcHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuDCFirst(ref PdfJsHuffmanTable dcHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int row, int col, DoubleBufferedStreamReader stream) { - int mcuRow = mcu / mcusPerLine; - int mcuCol = mcu % mcusPerLine; + int mcuRow = this.mcu / this.mcusPerLine; + int mcuCol = this.mcu % this.mcusPerLine; int blockRow = (mcuRow * component.VerticalSamplingFactor) + row; int blockCol = (mcuCol * component.HorizontalSamplingFactor) + col; int offset = component.GetBlockBufferOffset(blockRow, blockCol); @@ -371,19 +353,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockDCSuccessive(PdfJsFrameComponent component, ref short blockDataRef, int mcu, Stream stream) + private void DecodeBlockDCSuccessive(PdfJsFrameComponent component, ref short blockDataRef, DoubleBufferedStreamReader stream) { - int blockRow = mcu / component.WidthInBlocks; - int blockCol = mcu % component.WidthInBlocks; + int blockRow = this.mcu / component.WidthInBlocks; + int blockCol = this.mcu % component.WidthInBlocks; int offset = component.GetBlockBufferOffset(blockRow, blockCol); this.DecodeDCSuccessive(component, ref blockDataRef, offset, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuDCSuccessive(PdfJsFrameComponent component, ref short blockDataRef, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuDCSuccessive(PdfJsFrameComponent component, ref short blockDataRef, int row, int col, DoubleBufferedStreamReader stream) { - int mcuRow = mcu / mcusPerLine; - int mcuCol = mcu % mcusPerLine; + int mcuRow = this.mcu / this.mcusPerLine; + int mcuCol = this.mcu % this.mcusPerLine; int blockRow = (mcuRow * component.VerticalSamplingFactor) + row; int blockCol = (mcuCol * component.HorizontalSamplingFactor) + col; int offset = component.GetBlockBufferOffset(blockRow, blockCol); @@ -391,169 +373,257 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockACFirst(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcu, Stream stream) + private void DecodeBlockACFirst(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, DoubleBufferedStreamReader stream) { - int blockRow = mcu / component.WidthInBlocks; - int blockCol = mcu % component.WidthInBlocks; + int blockRow = this.mcu / component.WidthInBlocks; + int blockCol = this.mcu % component.WidthInBlocks; int offset = component.GetBlockBufferOffset(blockRow, blockCol); - this.DecodeACFirst(component, ref blockDataRef, offset, ref acHuffmanTable, stream); + this.DecodeACFirst(ref blockDataRef, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuACFirst(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuACFirst(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int row, int col, DoubleBufferedStreamReader stream) { - int mcuRow = mcu / mcusPerLine; - int mcuCol = mcu % mcusPerLine; + int mcuRow = this.mcu / this.mcusPerLine; + int mcuCol = this.mcu % this.mcusPerLine; int blockRow = (mcuRow * component.VerticalSamplingFactor) + row; int blockCol = (mcuCol * component.HorizontalSamplingFactor) + col; int offset = component.GetBlockBufferOffset(blockRow, blockCol); - this.DecodeACFirst(component, ref blockDataRef, offset, ref acHuffmanTable, stream); + this.DecodeACFirst(ref blockDataRef, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockACSuccessive(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcu, Stream stream) + private void DecodeBlockACSuccessive(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, DoubleBufferedStreamReader stream) { - int blockRow = mcu / component.WidthInBlocks; - int blockCol = mcu % component.WidthInBlocks; + int blockRow = this.mcu / component.WidthInBlocks; + int blockCol = this.mcu % component.WidthInBlocks; int offset = component.GetBlockBufferOffset(blockRow, blockCol); - this.DecodeACSuccessive(component, ref blockDataRef, offset, ref acHuffmanTable, stream); + this.DecodeACSuccessive(ref blockDataRef, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuACSuccessive(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuACSuccessive(ref PdfJsHuffmanTable acHuffmanTable, PdfJsFrameComponent component, ref short blockDataRef, int row, int col, DoubleBufferedStreamReader stream) { - int mcuRow = mcu / mcusPerLine; - int mcuCol = mcu % mcusPerLine; + int mcuRow = this.mcu / this.mcusPerLine; + int mcuCol = this.mcu % this.mcusPerLine; int blockRow = (mcuRow * component.VerticalSamplingFactor) + row; int blockCol = (mcuCol * component.HorizontalSamplingFactor) + col; int offset = component.GetBlockBufferOffset(blockRow, blockCol); - this.DecodeACSuccessive(component, ref blockDataRef, offset, ref acHuffmanTable, stream); + this.DecodeACSuccessive(ref blockDataRef, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int ReadBit(Stream stream) + private bool TryReadBit(DoubleBufferedStreamReader stream, out int bit) { - // TODO: I wonder if we can do this two bytes at a time; libjpeg turbo seems to do that? - if (this.bitsCount > 0) + if (this.bitsCount == 0) { - this.bitsCount--; - return (this.bitsData >> this.bitsCount) & 1; + if (!this.TryFillBits(stream)) + { + bit = 0; + return false; + } } - this.bitsData = stream.ReadByte(); + this.bitsCount--; + bit = (this.bitsData >> this.bitsCount) & 1; + return true; + } - if (this.bitsData == -0x1) - { - // We've encountered the end of the file stream which means there's no EOI marker ref the image - this.endOfStreamReached = true; - } + [MethodImpl(MethodImplOptions.NoInlining)] + private bool TryFillBits(DoubleBufferedStreamReader stream) + { + // TODO: Read more then 1 byte at a time. + // In LibJpegTurbo this is be 25 bits (32-7) but I cannot get this to work + // for some images, I'm assuming because I am crossing MCU boundaries and not maintining the correct buffer state. + const int MinGetBits = 7; - if (this.bitsData == PdfJsJpegConstants.Markers.Prefix) + if (!this.unexpectedMarkerReached) { - int nextByte = stream.ReadByte(); - if (nextByte != 0) + // Attempt to load to the minimum bit count. + while (this.bitsCount < MinGetBits) { + int c = stream.ReadByte(); + + switch (c) + { + case -0x1: + + // We've encountered the end of the file stream which means there's no EOI marker in the image. + this.endOfStreamReached = true; + return false; + + case PdfJsJpegConstants.Markers.Prefix: + int nextByte = stream.ReadByte(); + + if (nextByte == -0x1) + { + this.endOfStreamReached = true; + return false; + } + + if (nextByte != 0) + { #if DEBUG - Debug.WriteLine($"DecodeScan - Unexpected marker {(this.bitsData << 8) | nextByte:X} at {stream.Position}"); + Debug.WriteLine($"DecodeScan - Unexpected marker {(c << 8) | nextByte:X} at {stream.Position}"); #endif - // We've encountered an unexpected marker. Reverse the stream and exit. - this.unexpectedMarkerReached = true; - stream.Position -= 2; - } + // We've encountered an unexpected marker. Reverse the stream and exit. + this.unexpectedMarkerReached = true; + stream.Position -= 2; + + // TODO: double check we need this. + // Fill buffer with zero bits. + if (this.bitsCount == 0) + { + this.bitsData <<= MinGetBits; + this.bitsCount = MinGetBits; + } + + return true; + } - // Unstuff 0 + break; + } + + // OK, load the next byte into bitsData + this.bitsData = (this.bitsData << 8) | c; + this.bitsCount += 8; + } } - this.bitsCount = 7; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int PeekBits(int count) + { + return this.bitsData >> (this.bitsCount - count) & ((1 << count) - 1); + } - return this.bitsData >> 7; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DropBits(int count) + { + this.bitsCount -= count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private short DecodeHuffman(ref PdfJsHuffmanTable tree, Stream stream) + private bool TryDecodeHuffman(ref PdfJsHuffmanTable tree, DoubleBufferedStreamReader stream, out short value) { + value = -1; + // TODO: Implement fast Huffman decoding. - // NOTES # During investigation of the libjpeg implementation it appears that they pull 32bits at a time and operate on those bits - // using 3 methods: FillBits, PeekBits, and ReadBits. We should attempt to do the same. - short code = (short)this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + // In LibJpegTurbo a minimum of 25 bits (32-7) is collected from the stream + // Then a LUT is used to avoid the loop when decoding the Huffman value. + // using 3 methods: FillBits, PeekBits, and DropBits. + // The LUT has been ported from LibJpegTurbo as has this code but it doesn't work. + // this.TryFillBits(stream); + // + // const int LookAhead = 8; + // int look = this.PeekBits(LookAhead); + // look = tree.Lookahead[look]; + // int bits = look >> LookAhead; + // + // if (bits <= LookAhead) + // { + // this.DropBits(bits); + // value = (short)(look & ((1 << LookAhead) - 1)); + // return true; + // } + if (!this.TryReadBit(stream, out int bit)) { - return -1; + return false; } + short code = (short)bit; + // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 int i = 1; while (code > tree.MaxCode[i]) { - code <<= 1; - code |= (short)this.ReadBit(stream); - - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryReadBit(stream, out bit)) { - return -1; + return false; } + code <<= 1; + code |= (short)bit; i++; } int j = tree.ValOffset[i]; - return tree.HuffVal[(j + code) & 0xFF]; + value = tree.HuffVal[(j + code) & 0xFF]; + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int Receive(int length, Stream stream) + private bool TryReceive(int length, DoubleBufferedStreamReader stream, out int value) { - int n = 0; + value = 0; while (length > 0) { - int bit = this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryReadBit(stream, out int bit)) { - return -1; + return false; } - n = (n << 1) | bit; + value = (value << 1) | bit; length--; } - return n; + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int ReceiveAndExtend(int length, Stream stream) + private bool TryReceiveAndExtend(int length, DoubleBufferedStreamReader stream, out int value) { if (length == 1) { - return this.ReadBit(stream) == 1 ? 1 : -1; - } + if (!this.TryReadBit(stream, out value)) + { + return false; + } - int n = this.Receive(length, stream); - if (n >= 1 << (length - 1)) + value = value == 1 ? 1 : -1; + } + else { - return n; + if (!this.TryReceive(length, stream, out value)) + { + return false; + } + + if (value < 1 << (length - 1)) + { + value += (-1 << length) + 1; + } } - return n + (-1 << length) + 1; + return true; } - private void DecodeBaseline(PdfJsFrameComponent component, ref short blockDataRef, int offset, ref PdfJsHuffmanTable dcHuffmanTable, ref PdfJsHuffmanTable acHuffmanTable, Stream stream) + private void DecodeBaseline(PdfJsFrameComponent component, ref short blockDataRef, int offset, ref PdfJsHuffmanTable dcHuffmanTable, ref PdfJsHuffmanTable acHuffmanTable, DoubleBufferedStreamReader stream) { - short t = this.DecodeHuffman(ref dcHuffmanTable, stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryDecodeHuffman(ref dcHuffmanTable, stream, out short t)) { return; } - int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream); + int diff = 0; + if (t != 0) + { + if (!this.TryReceiveAndExtend(t, stream, out diff)) + { + return; + } + } + Unsafe.Add(ref blockDataRef, offset) = (short)(component.Pred += diff); int k = 1; while (k < 64) { - short rs = this.DecodeHuffman(ref acHuffmanTable, stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryDecodeHuffman(ref acHuffmanTable, stream, out short rs)) { return; } @@ -574,36 +644,42 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components k += r; - if (k > 63) + byte z = this.dctZigZag[k]; + + if (!this.TryReceiveAndExtend(s, stream, out int re)) { - break; + return; } - byte z = this.dctZigZag[k]; - short re = (short)this.ReceiveAndExtend(s, stream); - Unsafe.Add(ref blockDataRef, offset + z) = re; + Unsafe.Add(ref blockDataRef, offset + z) = (short)re; k++; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeDCFirst(PdfJsFrameComponent component, ref short blockDataRef, int offset, ref PdfJsHuffmanTable dcHuffmanTable, Stream stream) + private void DecodeDCFirst(PdfJsFrameComponent component, ref short blockDataRef, int offset, ref PdfJsHuffmanTable dcHuffmanTable, DoubleBufferedStreamReader stream) { - short t = this.DecodeHuffman(ref dcHuffmanTable, stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryDecodeHuffman(ref dcHuffmanTable, stream, out short t)) { return; } - int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream) << this.successiveState; - Unsafe.Add(ref blockDataRef, offset) = (short)(component.Pred += diff); + int diff = 0; + if (t != 0) + { + if (!this.TryReceiveAndExtend(t, stream, out diff)) + { + return; + } + } + + Unsafe.Add(ref blockDataRef, offset) = (short)(component.Pred += diff << this.successiveState); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeDCSuccessive(PdfJsFrameComponent component, ref short blockDataRef, int offset, Stream stream) + private void DecodeDCSuccessive(PdfJsFrameComponent component, ref short blockDataRef, int offset, DoubleBufferedStreamReader stream) { - int bit = this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryReadBit(stream, out int bit)) { return; } @@ -611,7 +687,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components Unsafe.Add(ref blockDataRef, offset) |= (short)(bit << this.successiveState); } - private void DecodeACFirst(PdfJsFrameComponent component, ref short blockDataRef, int offset, ref PdfJsHuffmanTable acHuffmanTable, Stream stream) + private void DecodeACFirst(ref short blockDataRef, int offset, ref PdfJsHuffmanTable acHuffmanTable, DoubleBufferedStreamReader stream) { if (this.eobrun > 0) { @@ -623,8 +699,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components int e = this.specEnd; while (k <= e) { - short rs = this.DecodeHuffman(ref acHuffmanTable, stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryDecodeHuffman(ref acHuffmanTable, stream, out short rs)) { return; } @@ -636,7 +711,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components { if (r < 15) { - this.eobrun = this.Receive(r, stream) + (1 << r) - 1; + if (!this.TryReceive(r, stream, out int eob)) + { + return; + } + + this.eobrun = eob + (1 << r) - 1; break; } @@ -647,12 +727,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components k += r; byte z = this.dctZigZag[k]; - Unsafe.Add(ref blockDataRef, offset + z) = (short)(this.ReceiveAndExtend(s, stream) * (1 << this.successiveState)); + + if (!this.TryReceiveAndExtend(s, stream, out int v)) + { + return; + } + + Unsafe.Add(ref blockDataRef, offset + z) = (short)(v * (1 << this.successiveState)); k++; } } - private void DecodeACSuccessive(PdfJsFrameComponent component, ref short blockDataRef, int offset, ref PdfJsHuffmanTable acHuffmanTable, Stream stream) + private void DecodeACSuccessive(ref short blockDataRef, int offset, ref PdfJsHuffmanTable acHuffmanTable, DoubleBufferedStreamReader stream) { int k = this.specStart; int e = this.specEnd; @@ -667,8 +753,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components switch (this.successiveACState) { case 0: // Initial state - short rs = this.DecodeHuffman(ref acHuffmanTable, stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + + if (!this.TryDecodeHuffman(ref acHuffmanTable, stream, out short rs)) { return; } @@ -679,7 +765,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components { if (r < 15) { - this.eobrun = this.Receive(r, stream) + (1 << r); + if (!this.TryReceive(r, stream, out int eob)) + { + return; + } + + this.eobrun = eob + (1 << r); this.successiveACState = 4; } else @@ -695,7 +786,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components throw new ImageFormatException("Invalid ACn encoding"); } - this.successiveACNextValue = this.ReceiveAndExtend(s, stream); + if (!this.TryReceiveAndExtend(s, stream, out int v)) + { + return; + } + + this.successiveACNextValue = v; this.successiveACState = r > 0 ? 2 : 3; } @@ -704,8 +800,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components case 2: if (blockOffsetZRef != 0) { - int bit = this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryReadBit(stream, out int bit)) { return; } @@ -725,8 +820,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components case 3: // Set value for a zero item if (blockOffsetZRef != 0) { - int bit = this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryReadBit(stream, out int bit)) { return; } @@ -743,8 +837,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components case 4: // Eob if (blockOffsetZRef != 0) { - int bit = this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) + if (!this.TryReadBit(stream, out int bit)) { return; } diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegConstants.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegConstants.cs index 2c369d390..437f77286 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegConstants.cs @@ -22,98 +22,98 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// /// The Start of Image marker /// - public const ushort SOI = 0xFFD8; + public const byte SOI = 0xD8; /// /// The End of Image marker /// - public const ushort EOI = 0xFFD9; + public const byte EOI = 0xD9; /// /// Application specific marker for marking the jpeg format. /// /// - public const ushort APP0 = 0xFFE0; + public const byte APP0 = 0xE0; /// /// Application specific marker for marking where to store metadata. /// - public const ushort APP1 = 0xFFE1; + public const byte APP1 = 0xE1; /// /// Application specific marker for marking where to store ICC profile information. /// - public const ushort APP2 = 0xFFE2; + public const byte APP2 = 0xE2; /// /// Application specific marker. /// - public const ushort APP3 = 0xFFE3; + public const byte APP3 = 0xE3; /// /// Application specific marker. /// - public const ushort APP4 = 0xFFE4; + public const byte APP4 = 0xE4; /// /// Application specific marker. /// - public const ushort APP5 = 0xFFE5; + public const byte APP5 = 0xE5; /// /// Application specific marker. /// - public const ushort APP6 = 0xFFE6; + public const byte APP6 = 0xE6; /// /// Application specific marker. /// - public const ushort APP7 = 0xFFE7; + public const byte APP7 = 0xE7; /// /// Application specific marker. /// - public const ushort APP8 = 0xFFE8; + public const byte APP8 = 0xE8; /// /// Application specific marker. /// - public const ushort APP9 = 0xFFE9; + public const byte APP9 = 0xE9; /// /// Application specific marker. /// - public const ushort APP10 = 0xFFEA; + public const byte APP10 = 0xEA; /// /// Application specific marker. /// - public const ushort APP11 = 0xFFEB; + public const byte APP11 = 0xEB; /// /// Application specific marker. /// - public const ushort APP12 = 0xFFEC; + public const byte APP12 = 0xEC; /// /// Application specific marker. /// - public const ushort APP13 = 0xFFED; + public const byte APP13 = 0xED; /// /// Application specific marker used by Adobe for storing encoding information for DCT filters. /// - public const ushort APP14 = 0xFFEE; + public const byte APP14 = 0xEE; /// /// Application specific marker used by GraphicConverter to store JPEG quality. /// - public const ushort APP15 = 0xFFEF; + public const byte APP15 = 0xEF; /// /// The text comment marker /// - public const ushort COM = 0xFFFE; + public const byte COM = 0xFE; /// /// Define Quantization Table(s) marker @@ -121,7 +121,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// Specifies one or more quantization tables. /// /// - public const ushort DQT = 0xFFDB; + public const byte DQT = 0xDB; /// /// Start of Frame (baseline DCT) @@ -130,7 +130,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// and component subsampling (e.g., 4:2:0). /// /// - public const ushort SOF0 = 0xFFC0; + public const byte SOF0 = 0xC0; /// /// Start Of Frame (Extended Sequential DCT) @@ -139,7 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// and component subsampling (e.g., 4:2:0). /// /// - public const ushort SOF1 = 0xFFC1; + public const byte SOF1 = 0xC1; /// /// Start Of Frame (progressive DCT) @@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// and component subsampling (e.g., 4:2:0). /// /// - public const ushort SOF2 = 0xFFC2; + public const byte SOF2 = 0xC2; /// /// Define Huffman Table(s) @@ -156,7 +156,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// Specifies one or more Huffman tables. /// /// - public const ushort DHT = 0xFFC4; + public const byte DHT = 0xC4; /// /// Define Restart Interval @@ -164,7 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// Specifies the interval between RSTn markers, in macroblocks.This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment. /// /// - public const ushort DRI = 0xFFDD; + public const byte DRI = 0xDD; /// /// Start of Scan @@ -174,7 +174,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// will contain, and is immediately followed by entropy-coded data. /// /// - public const ushort SOS = 0xFFDA; + public const byte SOS = 0xDA; /// /// Define First Restart @@ -183,7 +183,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// 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; + public const byte RST0 = 0xD0; /// /// Define Eigth Restart @@ -192,7 +192,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// 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; + public const byte RST7 = 0xD7; /// /// Contains Adobe specific markers diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs index 244d97fba..df803a920 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs @@ -22,7 +22,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort { /// /// Performs the jpeg decoding operation. - /// Ported from with additional fixes to handle common encoding errors + /// Originally ported from + /// with additional fixes for both performance and common encoding errors. /// internal sealed class PdfJsJpegDecoderCore : IRawJpegData { @@ -31,7 +32,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// public const int SupportedPrecision = 8; -#pragma warning disable SA1401 // Fields should be private /// /// The global configuration /// @@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// /// Gets the input stream. /// - public Stream InputStream { get; private set; } + public DoubleBufferedStreamReader InputStream { get; private set; } /// /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -144,7 +144,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// The buffer to read file markers to /// The input stream /// The - public static PdfJsFileMarker FindNextFileMarker(byte[] marker, Stream stream) + public static PdfJsFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStreamReader stream) { int value = stream.Read(marker, 0, 2); @@ -157,7 +157,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort { // According to Section B.1.1.2: // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." - while (marker[1] == PdfJsJpegConstants.Markers.Prefix) + int m = marker[1]; + while (m == PdfJsJpegConstants.Markers.Prefix) { int suffix = stream.ReadByte(); if (suffix == -1) @@ -165,13 +166,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort return new PdfJsFileMarker(PdfJsJpegConstants.Markers.EOI, stream.Length - 2); } - marker[1] = (byte)suffix; + m = suffix; } - return new PdfJsFileMarker(BinaryPrimitives.ReadUInt16BigEndian(marker), stream.Position - 2); + return new PdfJsFileMarker((byte)m, stream.Position - 2); } - return new PdfJsFileMarker(BinaryPrimitives.ReadUInt16BigEndian(marker), stream.Position - 2, true); + return new PdfJsFileMarker(marker[1], stream.Position - 2, true); } /// @@ -184,6 +185,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort where TPixel : struct, IPixel { this.ParseStream(stream); + this.AssignResolution(); return this.PostProcessIntoImage(); } @@ -206,136 +208,142 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort public void ParseStream(Stream stream, bool metadataOnly = false) { this.MetaData = new ImageMetaData(); - this.InputStream = stream; + this.InputStream = new DoubleBufferedStreamReader(this.configuration.MemoryManager, stream); // Check for the Start Of Image marker. - var fileMarker = new PdfJsFileMarker(this.ReadUint16(), 0); + this.InputStream.Read(this.markerBuffer, 0, 2); + var fileMarker = new PdfJsFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != PdfJsJpegConstants.Markers.SOI) { throw new ImageFormatException("Missing SOI marker."); } - ushort marker = this.ReadUint16(); + this.InputStream.Read(this.markerBuffer, 0, 2); + byte marker = this.markerBuffer[1]; fileMarker = new PdfJsFileMarker(marker, (int)this.InputStream.Position - 2); - this.QuantizationTables = new Block8x8F[4]; - - // this.quantizationTables = new PdfJsQuantizationTables(this.configuration.MemoryManager); - this.dcHuffmanTables = new PdfJsHuffmanTables(); - this.acHuffmanTables = new PdfJsHuffmanTables(); + // Only assign what we need + if (!metadataOnly) + { + this.QuantizationTables = new Block8x8F[4]; + this.dcHuffmanTables = new PdfJsHuffmanTables(); + this.acHuffmanTables = new PdfJsHuffmanTables(); + } while (fileMarker.Marker != PdfJsJpegConstants.Markers.EOI) { - // Get the marker length - int remaining = this.ReadUint16() - 2; - - switch (fileMarker.Marker) + if (!fileMarker.Invalid) { - case PdfJsJpegConstants.Markers.APP0: - this.ProcessApplicationHeaderMarker(remaining); - break; + // Get the marker length + int remaining = this.ReadUint16() - 2; - case PdfJsJpegConstants.Markers.APP1: - this.ProcessApp1Marker(remaining); - break; - - case PdfJsJpegConstants.Markers.APP2: - this.ProcessApp2Marker(remaining); - break; - case PdfJsJpegConstants.Markers.APP3: - case PdfJsJpegConstants.Markers.APP4: - case PdfJsJpegConstants.Markers.APP5: - case PdfJsJpegConstants.Markers.APP6: - case PdfJsJpegConstants.Markers.APP7: - case PdfJsJpegConstants.Markers.APP8: - case PdfJsJpegConstants.Markers.APP9: - case PdfJsJpegConstants.Markers.APP10: - case PdfJsJpegConstants.Markers.APP11: - case PdfJsJpegConstants.Markers.APP12: - case PdfJsJpegConstants.Markers.APP13: - this.InputStream.Skip(remaining); - break; - - case PdfJsJpegConstants.Markers.APP14: - this.ProcessApp14Marker(remaining); - break; + switch (fileMarker.Marker) + { + case PdfJsJpegConstants.Markers.SOF0: + case PdfJsJpegConstants.Markers.SOF1: + case PdfJsJpegConstants.Markers.SOF2: + this.ProcessStartOfFrameMarker(remaining, fileMarker, metadataOnly); + break; + + case PdfJsJpegConstants.Markers.SOS: + if (!metadataOnly) + { + this.ProcessStartOfScanMarker(); + break; + } + else + { + // It's highly unlikely that APPn related data will be found after the SOS marker + // We should have gathered everything we need by now. + return; + } - case PdfJsJpegConstants.Markers.APP15: - case PdfJsJpegConstants.Markers.COM: - this.InputStream.Skip(remaining); - break; + case PdfJsJpegConstants.Markers.DHT: + if (metadataOnly) + { + this.InputStream.Skip(remaining); + } + else + { + this.ProcessDefineHuffmanTablesMarker(remaining); + } - case PdfJsJpegConstants.Markers.DQT: - if (metadataOnly) - { - this.InputStream.Skip(remaining); - } - else - { - this.ProcessDefineQuantizationTablesMarker(remaining); - } + break; - break; + case PdfJsJpegConstants.Markers.DQT: + if (metadataOnly) + { + this.InputStream.Skip(remaining); + } + else + { + this.ProcessDefineQuantizationTablesMarker(remaining); + } - case PdfJsJpegConstants.Markers.SOF0: - case PdfJsJpegConstants.Markers.SOF1: - case PdfJsJpegConstants.Markers.SOF2: - this.ProcessStartOfFrameMarker(remaining, fileMarker); - if (metadataOnly && !this.jFif.Equals(default)) - { - this.InputStream.Skip(remaining); - } + break; - break; + case PdfJsJpegConstants.Markers.DRI: + if (metadataOnly) + { + this.InputStream.Skip(remaining); + } + else + { + this.ProcessDefineRestartIntervalMarker(remaining); + } - case PdfJsJpegConstants.Markers.DHT: - if (metadataOnly) - { + break; + + case PdfJsJpegConstants.Markers.APP0: + this.ProcessApplicationHeaderMarker(remaining); + break; + + case PdfJsJpegConstants.Markers.APP1: + this.ProcessApp1Marker(remaining); + break; + + case PdfJsJpegConstants.Markers.APP2: + this.ProcessApp2Marker(remaining); + break; + + case PdfJsJpegConstants.Markers.APP3: + case PdfJsJpegConstants.Markers.APP4: + case PdfJsJpegConstants.Markers.APP5: + case PdfJsJpegConstants.Markers.APP6: + case PdfJsJpegConstants.Markers.APP7: + case PdfJsJpegConstants.Markers.APP8: + case PdfJsJpegConstants.Markers.APP9: + case PdfJsJpegConstants.Markers.APP10: + case PdfJsJpegConstants.Markers.APP11: + case PdfJsJpegConstants.Markers.APP12: + case PdfJsJpegConstants.Markers.APP13: this.InputStream.Skip(remaining); - } - else - { - this.ProcessDefineHuffmanTablesMarker(remaining); - } + break; - break; + case PdfJsJpegConstants.Markers.APP14: + this.ProcessApp14Marker(remaining); + break; - case PdfJsJpegConstants.Markers.DRI: - if (metadataOnly) - { + case PdfJsJpegConstants.Markers.APP15: + case PdfJsJpegConstants.Markers.COM: this.InputStream.Skip(remaining); - } - else - { - this.ProcessDefineRestartIntervalMarker(remaining); - } - - break; - - case PdfJsJpegConstants.Markers.SOS: - if (!metadataOnly) - { - this.ProcessStartOfScanMarker(); - } - - break; + break; + } } // Read on. fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream); } - - this.ImageWidth = this.Frame.SamplesPerLine; - this.ImageHeight = this.Frame.Scanlines; - this.ComponentCount = this.Frame.ComponentCount; } /// public void Dispose() { + this.InputStream?.Dispose(); this.Frame?.Dispose(); // Set large fields to null. + this.InputStream = null; this.Frame = null; this.dcHuffmanTables = null; this.acHuffmanTables = null; @@ -379,7 +387,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// private void AssignResolution() { - if (this.isExif) + this.ImageWidth = this.Frame.SamplesPerLine; + this.ImageHeight = this.Frame.Scanlines; + + if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0) + { + this.MetaData.HorizontalResolution = this.jFif.XDensity; + this.MetaData.VerticalResolution = this.jFif.YDensity; + } + else if (this.isExif) { double horizontalValue = this.MetaData.ExifProfile.TryGetValue(ExifTag.XResolution, out ExifValue horizontalTag) ? ((Rational)horizontalTag.Value).ToDouble() @@ -395,11 +411,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort this.MetaData.VerticalResolution = verticalValue; } } - else if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0) - { - this.MetaData.HorizontalResolution = this.jFif.XDensity; - this.MetaData.VerticalResolution = this.jFif.YDensity; - } } /// @@ -593,7 +604,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// /// The remaining bytes in the segment block. /// The current frame marker. - private void ProcessStartOfFrameMarker(int remaining, PdfJsFileMarker frameMarker) + /// Whether to parse metadata only + private void ProcessStartOfFrameMarker(int remaining, PdfJsFileMarker frameMarker, bool metadataOnly) { if (this.Frame != null) { @@ -622,11 +634,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort int maxV = 0; int index = 6; - // No need to pool this. They max out at 4 - this.Frame.ComponentIds = new byte[this.Frame.ComponentCount]; - this.Frame.Components = new PdfJsFrameComponent[this.Frame.ComponentCount]; + this.ComponentCount = this.Frame.ComponentCount; + if (!metadataOnly) + { + // No need to pool this. They max out at 4 + this.Frame.ComponentIds = new byte[this.Frame.ComponentCount]; + this.Frame.Components = new PdfJsFrameComponent[this.Frame.ComponentCount]; + } - for (int i = 0; i < this.Frame.Components.Length; i++) + for (int i = 0; i < this.Frame.ComponentCount; i++) { byte hv = this.temp[index + 1]; int h = hv >> 4; @@ -642,17 +658,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort maxV = v; } - var component = new PdfJsFrameComponent(this.configuration.MemoryManager, this.Frame, this.temp[index], h, v, this.temp[index + 2], i); + if (!metadataOnly) + { + var component = new PdfJsFrameComponent(this.configuration.MemoryManager, this.Frame, this.temp[index], h, v, this.temp[index + 2], i); - this.Frame.Components[i] = component; - this.Frame.ComponentIds[i] = component.Id; + this.Frame.Components[i] = component; + this.Frame.ComponentIds[i] = component.Id; + } index += 3; } this.Frame.MaxHorizontalFactor = maxH; this.Frame.MaxVerticalFactor = maxV; - this.Frame.InitComponents(); + + if (!metadataOnly) + { + this.Frame.InitComponents(); + } } /// @@ -799,7 +822,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort where TPixel : struct, IPixel { this.ColorSpace = this.DeduceJpegColorSpace(); - this.AssignResolution(); using (var postProcessor = new JpegImagePostProcessor(this.configuration.MemoryManager, this)) { var image = new Image(this.configuration, this.ImageWidth, this.ImageHeight, this.MetaData); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs new file mode 100644 index 000000000..1d76d58a5 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg +{ + [Config(typeof(Config.ShortClr))] + public class DoubleBufferedStreams + { + private byte[] buffer = CreateTestBytes(); + private byte[] chunk1 = new byte[2]; + private byte[] chunk2 = new byte[2]; + + private MemoryStream stream1; + private MemoryStream stream2; + private MemoryStream stream3; + private MemoryStream stream4; + DoubleBufferedStreamReader reader1; + DoubleBufferedStreamReader reader2; + + [GlobalSetup] + public void CreateStreams() + { + this.stream1 = new MemoryStream(this.buffer); + this.stream2 = new MemoryStream(this.buffer); + this.stream3 = new MemoryStream(this.buffer); + this.stream4 = new MemoryStream(this.buffer); + this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryManager, this.stream2); + this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryManager, this.stream2); + } + + [GlobalCleanup] + public void DestroyStreams() + { + this.stream1?.Dispose(); + this.stream2?.Dispose(); + this.stream3?.Dispose(); + this.stream4?.Dispose(); + this.reader1?.Dispose(); + this.reader2?.Dispose(); + } + + [Benchmark(Baseline = true)] + public int StandardStreamReadByte() + { + int r = 0; + Stream stream = this.stream1; + + for (int i = 0; i < stream.Length; i++) + { + r += stream.ReadByte(); + } + + return r; + } + + [Benchmark] + public int StandardStreamRead() + { + int r = 0; + Stream stream = this.stream1; + byte[] b = this.chunk1; + + for (int i = 0; i < stream.Length / 2; i++) + { + r += stream.Read(b, 0, 2); + } + + return r; + } + + [Benchmark] + public int DoubleBufferedStreamReadByte() + { + int r = 0; + DoubleBufferedStreamReader reader = this.reader1; + + for (int i = 0; i < reader.Length; i++) + { + r += reader.ReadByte(); + } + + return r; + } + + [Benchmark] + public int DoubleBufferedStreamRead() + { + int r = 0; + DoubleBufferedStreamReader reader = this.reader2; + byte[] b = this.chunk2; + + for (int i = 0; i < reader.Length / 2; i++) + { + r += reader.Read(b, 0, 2); + } + + return r; + } + + private static byte[] CreateTestBytes() + { + byte[] buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; + var random = new Random(); + random.NextBytes(buffer); + + return buffer; + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs new file mode 100644 index 000000000..c3c128100 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Jpeg.GolangPort; +using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg +{ + [Config(typeof(Config.ShortClr))] + public class IdentifyJpeg + { + private byte[] jpegBytes; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Jpeg.Baseline.Jpeg420Exif, TestImages.Jpeg.Baseline.Calliphora)] + public string TestImage { get; set; } + + [GlobalSetup] + public void ReadImages() + { + if (this.jpegBytes == null) + { + this.jpegBytes = File.ReadAllBytes(this.TestImageFullPath); + } + } + + [Benchmark] + public IImageInfo IdentifyGolang() + { + using (var memoryStream = new MemoryStream(this.jpegBytes)) + { + var decoder = new OrigJpegDecoder(); + + return decoder.Identify(Configuration.Default, memoryStream); + } + } + + [Benchmark] + public IImageInfo IdentifyPdfJs() + { + using (var memoryStream = new MemoryStream(this.jpegBytes)) + { + var decoder = new PdfJsJpegDecoder(); + return decoder.Identify(Configuration.Default, memoryStream); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs new file mode 100644 index 000000000..be71e554f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components; +using SixLabors.ImageSharp.Memory; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + public class DoubleBufferedStreamReaderTests + { + private readonly MemoryManager manager = Configuration.Default.MemoryManager; + + [Fact] + public void DoubleBufferedStreamReaderCanReadSingleByteFromOrigin() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + var reader = new DoubleBufferedStreamReader(this.manager, stream); + + Assert.Equal(expected[0], reader.ReadByte()); + + // We've read a whole chunk but increment by 1 in our reader. + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); + Assert.Equal(1, reader.Position); + } + } + + [Fact] + public void DoubleBufferedStreamReaderCanReadSubsequentSingleByteCorrectly() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + var reader = new DoubleBufferedStreamReader(this.manager, stream); + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], reader.ReadByte()); + Assert.Equal(i + 1, reader.Position); + + if (i < DoubleBufferedStreamReader.ChunkLength) + { + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); + } + else if (i >= DoubleBufferedStreamReader.ChunkLength && i < DoubleBufferedStreamReader.ChunkLength * 2) + { + // We should have advanced to the second chunk now. + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); + } + else + { + // We should have advanced to the third chunk now. + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); + } + } + } + } + + [Fact] + public void DoubleBufferedStreamReaderCanReadMultipleBytesFromOrigin() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] buffer = new byte[2]; + byte[] expected = stream.ToArray(); + var reader = new DoubleBufferedStreamReader(this.manager, stream); + + Assert.Equal(2, reader.Read(buffer, 0, 2)); + Assert.Equal(expected[0], buffer[0]); + Assert.Equal(expected[1], buffer[1]); + + // We've read a whole chunk but increment by the buffer length in our reader. + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); + Assert.Equal(buffer.Length, reader.Position); + } + } + + [Fact] + public void DoubleBufferedStreamReaderCanReadSubsequentMultipleByteCorrectly() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] buffer = new byte[2]; + byte[] expected = stream.ToArray(); + var reader = new DoubleBufferedStreamReader(this.manager, stream); + + for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) + { + + Assert.Equal(2, reader.Read(buffer, 0, 2)); + Assert.Equal(expected[o], buffer[0]); + Assert.Equal(expected[o + 1], buffer[1]); + Assert.Equal(o + 2, reader.Position); + + int offset = i * 2; + if (offset < DoubleBufferedStreamReader.ChunkLength) + { + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); + } + else if (offset >= DoubleBufferedStreamReader.ChunkLength && offset < DoubleBufferedStreamReader.ChunkLength * 2) + { + // We should have advanced to the second chunk now. + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); + } + else + { + // We should have advanced to the third chunk now. + Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); + } + } + } + } + + [Fact] + public void DoubleBufferedStreamReaderCanSkip() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + var reader = new DoubleBufferedStreamReader(this.manager, stream); + + int skip = 50; + int plusOne = 1; + int skip2 = DoubleBufferedStreamReader.ChunkLength; + + // Skip + reader.Skip(skip); + Assert.Equal(skip, reader.Position); + Assert.Equal(stream.Position, reader.Position); + + // Read + Assert.Equal(expected[skip], reader.ReadByte()); + + // Skip Again + reader.Skip(skip2); + + // First Skap + First Read + Second Skip + int position = skip + plusOne + skip2; + + Assert.Equal(position, reader.Position); + Assert.Equal(stream.Position, reader.Position); + Assert.Equal(expected[position], reader.ReadByte()); + } + } + + private MemoryStream CreateTestStream() + { + byte[] buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; + var random = new Random(); + random.NextBytes(buffer); + + return new MemoryStream(buffer); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs new file mode 100644 index 000000000..7fc949b09 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.MetaData.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + using System.Runtime.CompilerServices; + + using SixLabors.ImageSharp.Formats.Jpeg; + + public partial class JpegDecoderTests + { + // TODO: A JPEGsnoop & metadata expert should review if the Exif/Icc expectations are correct. + // I'm seeing several entries with Exif-related names in images where we do not decode an exif profile. (- Anton) + public static readonly TheoryData MetaDataTestData = + new TheoryData + { + { false, TestImages.Jpeg.Progressive.Progress, 24, false, false }, + { false, TestImages.Jpeg.Progressive.Fb, 24, false, true }, + { false, TestImages.Jpeg.Baseline.Cmyk, 32, false, true }, + { false, TestImages.Jpeg.Baseline.Ycck, 32, true, true }, + { false, TestImages.Jpeg.Baseline.Jpeg400, 8, false, false }, + { false, TestImages.Jpeg.Baseline.Snake, 24, true, true }, + { false, TestImages.Jpeg.Baseline.Jpeg420Exif, 24, true, false }, + + { true, TestImages.Jpeg.Progressive.Progress, 24, false, false }, + { true, TestImages.Jpeg.Progressive.Fb, 24, false, true }, + { true, TestImages.Jpeg.Baseline.Cmyk, 32, false, true }, + { true, TestImages.Jpeg.Baseline.Ycck, 32, true, true }, + { true, TestImages.Jpeg.Baseline.Jpeg400, 8, false, false }, + { true, TestImages.Jpeg.Baseline.Snake, 24, true, true }, + { true, TestImages.Jpeg.Baseline.Jpeg420Exif, 24, true, false }, + }; + + [Theory] + [MemberData(nameof(MetaDataTestData))] + public void MetaDataIsParsedCorrectly_Orig( + bool useIdentify, + string imagePath, + int expectedPixelSize, + bool exifProfilePresent, + bool iccProfilePresent) + { + TestMetaDataImpl( + useIdentify, + OrigJpegDecoder, + imagePath, + expectedPixelSize, + exifProfilePresent, + iccProfilePresent); + } + + [Theory] + [MemberData(nameof(MetaDataTestData))] + public void MetaDataIsParsedCorrectly_PdfJs( + bool useIdentify, + string imagePath, + int expectedPixelSize, + bool exifProfilePresent, + bool iccProfilePresent) + { + TestMetaDataImpl( + useIdentify, + PdfJsJpegDecoder, + imagePath, + expectedPixelSize, + exifProfilePresent, + iccProfilePresent); + } + + private static void TestMetaDataImpl( + bool useIdentify, + IImageDecoder decoder, + string imagePath, + int expectedPixelSize, + bool exifProfilePresent, + bool iccProfilePresent) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo imageInfo = useIdentify + ? ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream) + : decoder.Decode(Configuration.Default, stream); + + Assert.NotNull(imageInfo); + Assert.NotNull(imageInfo.PixelType); + + if (useIdentify) + { + Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); + } + else + { + // When full Image decoding is performed, BitsPerPixel will match TPixel + int bpp32 = Unsafe.SizeOf() * 8; + Assert.Equal(bpp32, imageInfo.PixelType.BitsPerPixel); + } + + ExifProfile exifProfile = imageInfo.MetaData.ExifProfile; + + if (exifProfilePresent) + { + Assert.NotNull(exifProfile); + Assert.NotEmpty(exifProfile.Values); + } + else + { + Assert.Null(exifProfile); + } + + IccProfile iccProfile = imageInfo.MetaData.IccProfile; + + if (iccProfilePresent) + { + Assert.NotNull(iccProfile); + Assert.NotEmpty(iccProfile.Entries); + } + else + { + Assert.Null(iccProfile); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void IgnoreMetaData_ControlsWhetherMetaDataIsParsed(bool ignoreMetaData) + { + var decoder = new JpegDecoder() { IgnoreMetadata = ignoreMetaData }; + + // Snake.jpg has both Exif and ICC profiles defined: + var testFile = TestFile.Create(TestImages.Jpeg.Baseline.Snake); + + using (Image image = testFile.CreateImage(decoder)) + { + if (ignoreMetaData) + { + Assert.Null(image.MetaData.ExifProfile); + Assert.Null(image.MetaData.IccProfile); + } + else + { + Assert.NotNull(image.MetaData.ExifProfile); + Assert.NotNull(image.MetaData.IccProfile); + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 0b8daac72..3138300b9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -22,7 +22,7 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Formats.Jpg { // TODO: Scatter test cases into multiple test classes - public class JpegDecoderTests + public partial class JpegDecoderTests { public static string[] BaselineTestJpegs = { @@ -115,9 +115,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private ITestOutputHelper Output { get; } - private static IImageDecoder OrigJpegDecoder => new OrigJpegDecoder(); + private static OrigJpegDecoder OrigJpegDecoder => new OrigJpegDecoder(); - private static IImageDecoder PdfJsJpegDecoder => new PdfJsJpegDecoder(); + private static PdfJsJpegDecoder PdfJsJpegDecoder => new PdfJsJpegDecoder(); [Fact] public void ParseStream_BasicPropertiesAreCorrect1_PdfJs() @@ -151,7 +151,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // For 32 bit test enviroments: provider.Configuration.MemoryManager = ArrayPoolMemoryManager.CreateWithModeratePooling(); - IImageDecoder decoder = useOldDecoder ? OrigJpegDecoder : PdfJsJpegDecoder; + IImageDecoder decoder = useOldDecoder ? (IImageDecoder)OrigJpegDecoder : PdfJsJpegDecoder; using (Image image = provider.GetImage(decoder)) { image.DebugSave(provider); @@ -406,39 +406,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(72, image.MetaData.VerticalResolution); } } - - [Fact] - public void Decode_IgnoreMetadataIsFalse_ExifProfileIsRead() - { - var decoder = new JpegDecoder() - { - IgnoreMetadata = false - }; - - var testFile = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan); - - using (Image image = testFile.CreateImage(decoder)) - { - Assert.NotNull(image.MetaData.ExifProfile); - } - } - - [Fact] - public void Decode_IgnoreMetadataIsTrue_ExifProfileIgnored() - { - var options = new JpegDecoder() - { - IgnoreMetadata = true - }; - - var testFile = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan); - - using (Image image = testFile.CreateImage(options)) - { - Assert.Null(image.MetaData.ExifProfile); - } - } - + // DEBUG ONLY! // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // into "\tests\Images\ActualOutput\JpegDecoderTests\" @@ -470,37 +438,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.Output.WriteLine($"Difference for PORT: {portReport.DifferencePercentageString}"); } } - - [Theory] - [InlineData(TestImages.Jpeg.Progressive.Progress, 24)] - [InlineData(TestImages.Jpeg.Progressive.Fb, 24)] - [InlineData(TestImages.Jpeg.Baseline.Cmyk, 32)] - [InlineData(TestImages.Jpeg.Baseline.Ycck, 32)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg400, 8)] - [InlineData(TestImages.Jpeg.Baseline.Snake, 24)] - public void DetectPixelSizeGolang(string imagePath, int expectedPixelSize) - { - var testFile = TestFile.Create(imagePath); - using (var stream = new MemoryStream(testFile.Bytes, false)) - { - Assert.Equal(expectedPixelSize, ((IImageInfoDetector)OrigJpegDecoder).Identify(Configuration.Default, stream)?.PixelType?.BitsPerPixel); - } - } - - [Theory] - [InlineData(TestImages.Jpeg.Progressive.Progress, 24)] - [InlineData(TestImages.Jpeg.Progressive.Fb, 24)] - [InlineData(TestImages.Jpeg.Baseline.Cmyk, 32)] - [InlineData(TestImages.Jpeg.Baseline.Ycck, 32)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg400, 8)] - [InlineData(TestImages.Jpeg.Baseline.Snake, 24)] - public void DetectPixelSizePdfJs(string imagePath, int expectedPixelSize) - { - var testFile = TestFile.Create(imagePath); - using (var stream = new MemoryStream(testFile.Bytes, false)) - { - Assert.Equal(expectedPixelSize, ((IImageInfoDetector)PdfJsJpegDecoder).Identify(Configuration.Default, stream)?.PixelType?.BitsPerPixel); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs index 0d563a7b7..b665d69e8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { var expecteColorSpace = (JpegColorSpace)expectedColorSpaceValue; - using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, true)) + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, false)) { Assert.Equal(expecteColorSpace, decoder.ColorSpace); } @@ -42,11 +42,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Fact] public void ComponentScalingIsCorrect_1ChannelJpeg() { - using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(TestImages.Jpeg.Baseline.Jpeg400, true)) + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(TestImages.Jpeg.Baseline.Jpeg400, false)) { Assert.Equal(1, decoder.ComponentCount); Assert.Equal(1, decoder.Components.Length); - + Size expectedSizeInBlocks = decoder.ImageSizeInPixels.DivideRoundUp(8); Assert.Equal(expectedSizeInBlocks, decoder.ImageSizeInMCU); @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { var sb = new StringBuilder(); - using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, true)) + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, false)) { sb.AppendLine(imageFile); sb.AppendLine($"Size:{decoder.ImageSizeInPixels} MCU:{decoder.ImageSizeInMCU}"); @@ -103,23 +103,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Size fLuma = (Size)expectedLumaFactors; Size fChroma = (Size)expectedChromaFactors; - using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, true)) + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, false)) { Assert.Equal(componentCount, decoder.ComponentCount); Assert.Equal(componentCount, decoder.Components.Length); - + OrigComponent c0 = decoder.Components[0]; OrigComponent c1 = decoder.Components[1]; OrigComponent c2 = decoder.Components[2]; var uniform1 = new Size(1, 1); - Size expectedLumaSizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(fLuma) ; + Size expectedLumaSizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(fLuma); Size divisor = fLuma.DivideBy(fChroma); Size expectedChromaSizeInBlocks = expectedLumaSizeInBlocks.DivideRoundUp(divisor); - + VerifyJpeg.VerifyComponent(c0, expectedLumaSizeInBlocks, fLuma, uniform1); VerifyJpeg.VerifyComponent(c1, expectedChromaSizeInBlocks, fChroma, divisor); VerifyJpeg.VerifyComponent(c2, expectedChromaSizeInBlocks, fChroma, divisor);