diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index a09c7ada3e..70a4465121 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -18,11 +18,24 @@ internal class HuffmanScanDecoder { private readonly BufferedReadStream stream; - // Frame related + /// + /// instance containing decoding-related information. + /// private JpegFrame frame; + + /// + /// Shortcut for .Components. + /// private JpegComponent[] components; - // The restart interval. + /// + /// Number of component in the current scan. + /// + private int componentsCount; + + /// + /// The reset interval determined by RST markers. + /// private int restartInterval; // How many mcu's are left to do. @@ -31,6 +44,16 @@ internal class HuffmanScanDecoder // The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. private int eobrun; + /// + /// The DC Huffman tables. + /// + private readonly HuffmanTable[] dcHuffmanTables; + + /// + /// The AC Huffman tables + /// + private readonly HuffmanTable[] acHuffmanTables; + // The unzig data. private ZigZag dctZigZag; @@ -55,14 +78,16 @@ public HuffmanScanDecoder( this.stream = stream; this.spectralConverter = converter; this.cancellationToken = cancellationToken; - } - // huffman tables - public HuffmanTable[] DcHuffmanTables { get; set; } - - public HuffmanTable[] AcHuffmanTables { get; set; } + // TODO: this is actually a variable value depending on component count + const int maxTables = 4; + this.dcHuffmanTables = new HuffmanTable[maxTables]; + this.acHuffmanTables = new HuffmanTable[maxTables]; + } - // Reset interval + /// + /// Sets reset interval determined by RST markers. + /// public int ResetInterval { set @@ -72,9 +97,6 @@ public int ResetInterval } } - // The number of interleaved components. - public int ComponentsLength { get; set; } - // The spectral selection start. public int SpectralStart { get; set; } @@ -90,10 +112,12 @@ public int ResetInterval /// /// Decodes the entropy coded data. /// - public void ParseEntropyCodedData() + public void ParseEntropyCodedData(int componentCount) { this.cancellationToken.ThrowIfCancellationRequested(); + this.componentsCount = componentCount; + this.scanBuffer = new HuffmanScanBuffer(this.stream); bool fullScan = this.frame.Progressive || this.frame.MultiScan; @@ -124,7 +148,7 @@ public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) private void ParseBaselineData() { - if (this.ComponentsLength == this.frame.ComponentCount) + if (this.componentsCount == this.frame.ComponentCount) { this.ParseBaselineDataInterleaved(); } @@ -143,13 +167,13 @@ private void ParseBaselineDataInterleaved() ref HuffmanScanBuffer buffer = ref this.scanBuffer; // Pre-derive the huffman table to avoid in-loop checks. - for (int i = 0; i < this.ComponentsLength; i++) + for (int i = 0; i < this.componentsCount; i++) { int order = this.frame.ComponentOrder[i]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; dcHuffmanTable.Configure(); acHuffmanTable.Configure(); } @@ -163,13 +187,13 @@ private void ParseBaselineDataInterleaved() { // Scan an interleaved mcu... process components in order int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < this.ComponentsLength; k++) + for (int k = 0; k < this.componentsCount; k++) { int order = this.frame.ComponentOrder[k]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; int h = component.HorizontalSamplingFactor; int v = component.VerticalSamplingFactor; @@ -221,8 +245,8 @@ private void ParseBaselineDataNonInterleaved() int w = component.WidthInBlocks; int h = component.HeightInBlocks; - ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; dcHuffmanTable.Configure(); acHuffmanTable.Configure(); @@ -272,7 +296,7 @@ private void CheckProgressiveData() } // AC scans may have only one component. - if (this.ComponentsLength != 1) + if (this.componentsCount != 1) { invalid = true; } @@ -304,7 +328,7 @@ private void ParseProgressiveData() { this.CheckProgressiveData(); - if (this.ComponentsLength == 1) + if (this.componentsCount == 1) { this.ParseProgressiveDataNonInterleaved(); } @@ -323,11 +347,11 @@ private void ParseProgressiveDataInterleaved() ref HuffmanScanBuffer buffer = ref this.scanBuffer; // Pre-derive the huffman table to avoid in-loop checks. - for (int k = 0; k < this.ComponentsLength; k++) + for (int k = 0; k < this.componentsCount; k++) { int order = this.frame.ComponentOrder[k]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; dcHuffmanTable.Configure(); } @@ -338,11 +362,11 @@ private void ParseProgressiveDataInterleaved() // Scan an interleaved mcu... process components in order int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < this.ComponentsLength; k++) + for (int k = 0; k < this.componentsCount; k++) { int order = this.frame.ComponentOrder[k]; JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; int h = component.HorizontalSamplingFactor; int v = component.VerticalSamplingFactor; @@ -390,7 +414,7 @@ private void ParseProgressiveDataNonInterleaved() if (this.SpectralStart == 0) { - ref HuffmanTable dcHuffmanTable = ref this.DcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; dcHuffmanTable.Configure(); for (int j = 0; j < h; j++) @@ -418,7 +442,7 @@ ref Unsafe.Add(ref blockRef, i), } else { - ref HuffmanTable acHuffmanTable = ref this.AcHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; acHuffmanTable.Configure(); for (int j = 0; j < h; j++) @@ -722,5 +746,19 @@ private bool HandleRestart() return false; } + + /// + /// Build the huffman table using code lengths and code values. + /// + /// Table type. + /// Table index. + /// Code lengths. + /// Code values. + [MethodImpl(InliningOptions.ShortMethod)] + public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values) + { + HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables; + tables[index] = new HuffmanTable(codeLengths, values); + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs index b1ac1f78f5..391dac784f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs @@ -11,26 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal interface IRawJpegData : IDisposable { - /// - /// Gets the image size in pixels. - /// - Size ImageSizeInPixels { get; } - - /// - /// Gets the number of components. - /// - int ComponentCount { get; } - /// /// Gets the color space /// JpegColorSpace ColorSpace { get; } - /// - /// Gets the number of bits used for precision. - /// - int Precision { get; } - /// /// Gets the components. /// @@ -41,4 +26,4 @@ internal interface IRawJpegData : IDisposable /// Block8x8F[] QuantizationTables { get; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs index e0311dafef..7cfbaddcc1 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs @@ -38,11 +38,6 @@ internal struct JpegBlockPostProcessor /// private Size subSamplingDivisors; - /// - /// Defines the maximum value derived from the bitdepth. - /// - private readonly int maximumValue; - /// /// Initializes a new instance of the struct. /// @@ -53,7 +48,6 @@ public JpegBlockPostProcessor(IRawJpegData decoder, IJpegComponent component) int qtIndex = component.QuantizationTableIndex; this.DequantiazationTable = ZigZag.CreateDequantizationTable(ref decoder.QuantizationTables[qtIndex]); this.subSamplingDivisors = component.SubSamplingDivisors; - this.maximumValue = (int)MathF.Pow(2, decoder.Precision) - 1; this.SourceBlock = default; this.WorkspaceBlock1 = default; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs index 614e96e54a..ba3dfb6296 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -106,20 +106,24 @@ public void Dispose() this.SpectralBlocks = null; } - public void Init() + /// + /// Initializes component for future buffers initialization. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(int maxSubFactorH, int maxSubFactorV) { this.WidthInBlocks = (int)MathF.Ceiling( - MathF.Ceiling(this.Frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / this.Frame.MaxHorizontalFactor); + MathF.Ceiling(this.Frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / maxSubFactorH); this.HeightInBlocks = (int)MathF.Ceiling( - MathF.Ceiling(this.Frame.PixelHeight / 8F) * this.VerticalSamplingFactor / this.Frame.MaxVerticalFactor); + MathF.Ceiling(this.Frame.PixelHeight / 8F) * this.VerticalSamplingFactor / maxSubFactorV); int blocksPerLineForMcu = this.Frame.McusPerLine * this.HorizontalSamplingFactor; int blocksPerColumnForMcu = this.Frame.McusPerColumn * this.VerticalSamplingFactor; this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu); - JpegComponent c0 = this.Frame.Components[0]; - this.SubSamplingDivisors = c0.SamplingFactors.DivideBy(this.SamplingFactors); + this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors); if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index 79965a3f0c..9a659d6216 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -21,11 +21,18 @@ internal class JpegComponentPostProcessor : IDisposable /// private readonly Size blockAreaSize; + /// + /// Jpeg frame instance containing required decoding metadata. + /// + private readonly JpegFrame frame; + /// /// Initializes a new instance of the class. /// - public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) { + this.frame = frame; + this.Component = component; this.RawJpeg = rawJpeg; this.blockAreaSize = this.Component.SubSamplingDivisors * 8; @@ -70,7 +77,8 @@ public void CopyBlocksToColorBuffer(int step) Buffer2D spectralBuffer = this.Component.SpectralBlocks; var blockPp = new JpegBlockPostProcessor(this.RawJpeg, this.Component); - float maximumValue = MathF.Pow(2, this.RawJpeg.Precision) - 1; + + float maximumValue = this.frame.MaxColorChannelValue; int destAreaStride = this.ColorBuffer.Width; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs index 3a136b4103..fc109be261 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -10,15 +10,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal sealed class JpegFrame : IDisposable { + public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount) + { + this.Extended = sofMarker.Marker == JpegConstants.Markers.SOF1; + this.Progressive = sofMarker.Marker == JpegConstants.Markers.SOF2; + + this.Precision = precision; + this.MaxColorChannelValue = MathF.Pow(2, precision) - 1; + + this.PixelWidth = width; + this.PixelHeight = height; + + this.ComponentCount = componentCount; + } + /// - /// Gets or sets a value indicating whether the frame uses the extended specification. + /// Gets a value indicating whether the frame uses the extended specification. /// - public bool Extended { get; set; } + public bool Extended { get; private set; } /// - /// Gets or sets a value indicating whether the frame uses the progressive specification. + /// Gets a value indicating whether the frame uses the progressive specification. /// - public bool Progressive { get; set; } + public bool Progressive { get; private set; } /// /// Gets or sets a value indicating whether the frame is encoded using multiple scans (SOS markers). @@ -29,24 +43,34 @@ internal sealed class JpegFrame : IDisposable public bool MultiScan { get; set; } /// - /// Gets or sets the precision. + /// Gets the precision. + /// + public byte Precision { get; private set; } + + /// + /// Gets the maximum color value derived from . + /// + public float MaxColorChannelValue { get; private set; } + + /// + /// Gets the number of pixel per row. /// - public byte Precision { get; set; } + public int PixelHeight { get; private set; } /// - /// Gets or sets the number of scanlines within the frame. + /// Gets the number of pixels per line. /// - public int PixelHeight { get; set; } + public int PixelWidth { get; private set; } /// - /// Gets or sets the number of samples per scanline. + /// Gets the pixel size of the image. /// - public int PixelWidth { get; set; } + public Size PixelSize => new Size(this.PixelWidth, this.PixelHeight); /// - /// Gets or sets the number of components within a frame. In progressive frames this value can range from only 1 to 4. + /// Gets the number of components within a frame. /// - public byte ComponentCount { get; set; } + public byte ComponentCount { get; private set; } /// /// Gets or sets the component id collection. @@ -65,24 +89,24 @@ internal sealed class JpegFrame : IDisposable public JpegComponent[] Components { get; set; } /// - /// Gets or sets the maximum horizontal sampling factor. + /// Gets or sets the number of MCU's per line. /// - public int MaxHorizontalFactor { get; set; } + public int McusPerLine { get; set; } /// - /// Gets or sets the maximum vertical sampling factor. + /// Gets or sets the number of MCU's per column. /// - public int MaxVerticalFactor { get; set; } + public int McusPerColumn { get; set; } /// - /// Gets or sets the number of MCU's per line. + /// Gets the mcu size of the image. /// - public int McusPerLine { get; set; } + public Size McuSize => new Size(this.McusPerLine, this.McusPerColumn); /// - /// Gets or sets the number of MCU's per column. + /// Gets the color depth, in number of bits per pixel. /// - public int McusPerColumn { get; set; } + public int BitsPerPixel => this.ComponentCount * this.Precision; /// public void Dispose() @@ -101,15 +125,17 @@ public void Dispose() /// /// Allocates the frame component blocks. /// - public void InitComponents() + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(int maxSubFactorH, int maxSubFactorV) { - this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)this.MaxHorizontalFactor * 8); - this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)this.MaxVerticalFactor * 8); + this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)maxSubFactorH * 8); + this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)maxSubFactorV * 8); for (int i = 0; i < this.ComponentCount; i++) { JpegComponent component = this.Components[i]; - component.Init(); + component.Init(maxSubFactorH, maxSubFactorV); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 9f3d4195cc..50cfa0188a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -78,7 +78,7 @@ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length]; for (int i = 0; i < this.componentProcessors.Length; i++) { - this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, jpegData, postProcessorBufferSize, frame.Components[i]); + this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); } // single 'stride' rgba32 buffer for conversion between spectral and TPixel diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 922e9797cb..77b1b44aff 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -30,7 +30,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// /// The only supported precision /// - private readonly int[] supportedPrecisions = { 8, 12 }; + private readonly byte[] supportedPrecisions = { 8, 12 }; /// /// The buffer used to temporarily store bytes read from the stream. @@ -42,21 +42,6 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// private readonly byte[] markerBuffer = new byte[2]; - /// - /// The DC Huffman tables. - /// - private HuffmanTable[] dcHuffmanTables; - - /// - /// The AC Huffman tables - /// - private HuffmanTable[] acHuffmanTables; - - /// - /// The reset interval determined by RST markers. - /// - private ushort resetInterval; - /// /// Whether the image has an EXIF marker. /// @@ -122,30 +107,7 @@ public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) public JpegFrame Frame { get; private set; } /// - public Size ImageSizeInPixels { get; private set; } - - /// - Size IImageDecoderInternals.Dimensions => this.ImageSizeInPixels; - - /// - /// Gets the number of MCU blocks in the image as . - /// - public Size ImageSizeInMCU { get; private set; } - - /// - /// Gets the image width - /// - public int ImageWidth => this.ImageSizeInPixels.Width; - - /// - /// Gets the image height - /// - public int ImageHeight => this.ImageSizeInPixels.Height; - - /// - /// Gets the color depth, in number of bits per pixel. - /// - public int BitsPerPixel => this.ComponentCount * this.Frame.Precision; + Size IImageDecoderInternals.Dimensions => this.Frame.PixelSize; /// /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -157,15 +119,9 @@ public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) /// public ImageMetadata Metadata { get; private set; } - /// - public int ComponentCount { get; private set; } - /// public JpegColorSpace ColorSpace { get; private set; } - /// - public int Precision { get; private set; } - /// /// Gets the components. /// @@ -240,7 +196,8 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella this.InitIptcProfile(); this.InitDerivedMetadataProperties(); - return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.Metadata); + Size pixelSize = this.Frame.PixelSize; + return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); } /// @@ -270,14 +227,6 @@ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDeco fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); this.QuantizationTables = new Block8x8F[4]; - // Only assign what we need - if (!metadataOnly) - { - const int maxTables = 4; - this.dcHuffmanTables = new HuffmanTable[maxTables]; - this.acHuffmanTables = new HuffmanTable[maxTables]; - } - // Break only when we discover a valid EOI marker. // https://github.com/SixLabors/ImageSharp/issues/695 while (fileMarker.Marker != JpegConstants.Markers.EOI @@ -301,7 +250,7 @@ internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDeco case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream, cancellationToken); + this.ProcessStartOfScanMarker(stream, remaining, cancellationToken); break; } else @@ -392,22 +341,21 @@ public void Dispose() // Set large fields to null. this.Frame = null; - this.dcHuffmanTables = null; - this.acHuffmanTables = null; + this.scanDecoder = null; } /// /// Returns the correct colorspace based on the image component count /// /// The - private JpegColorSpace DeduceJpegColorSpace() + private JpegColorSpace DeduceJpegColorSpace(byte componentCount) { - if (this.ComponentCount == 1) + if (componentCount == 1) { return JpegColorSpace.Grayscale; } - if (this.ComponentCount == 3) + if (componentCount == 3) { if (!this.adobe.Equals(default) && this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) { @@ -419,14 +367,14 @@ private JpegColorSpace DeduceJpegColorSpace() return JpegColorSpace.YCbCr; } - if (this.ComponentCount == 4) + if (componentCount == 4) { return this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck ? JpegColorSpace.Ycck : JpegColorSpace.Cmyk; } - JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {this.ComponentCount}"); + JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {componentCount}"); return default; } @@ -565,7 +513,7 @@ private void ProcessApp1Marker(BufferedReadStream stream, int remaining) JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); } - var profile = new byte[remaining]; + byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker)) @@ -599,14 +547,14 @@ private void ProcessApp2Marker(BufferedReadStream stream, int remaining) return; } - var identifier = new byte[Icclength]; + byte[] identifier = new byte[Icclength]; stream.Read(identifier, 0, Icclength); remaining -= Icclength; // We have read it by this point if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) { this.isIcc = true; - var profile = new byte[remaining]; + byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); if (this.iccData is null) @@ -644,7 +592,7 @@ private void ProcessApp13Marker(BufferedReadStream stream, int remaining) remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length; if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker)) { - var resourceBlockData = new byte[remaining]; + byte[] resourceBlockData = new byte[remaining]; stream.Read(resourceBlockData, 0, remaining); Span blockDataSpan = resourceBlockData.AsSpan(); @@ -659,8 +607,8 @@ private void ProcessApp13Marker(BufferedReadStream stream, int remaining) Span imageResourceBlockId = blockDataSpan.Slice(0, 2); if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker)) { - var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); - var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + int resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + int resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); int dataStartIdx = 2 + resourceBlockNameLength + 4; if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize) { @@ -671,8 +619,8 @@ private void ProcessApp13Marker(BufferedReadStream stream, int remaining) } else { - var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); - var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + int resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + int resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); int dataStartIdx = 2 + resourceBlockNameLength + 4; if (blockDataSpan.Length < dataStartIdx + resourceDataSize) { @@ -695,7 +643,7 @@ private void ProcessApp13Marker(BufferedReadStream stream, int remaining) private static int ReadImageResourceNameLength(Span blockDataSpan) { byte nameLength = blockDataSpan[2]; - var nameDataSize = nameLength == 0 ? 2 : nameLength; + int nameDataSize = nameLength == 0 ? 2 : nameLength; if (nameDataSize % 2 != 0) { nameDataSize++; @@ -712,9 +660,7 @@ private static int ReadImageResourceNameLength(Span blockDataSpan) /// The block length. [MethodImpl(InliningOptions.ShortMethod)] private static int ReadResourceDataLength(Span blockDataSpan, int resourceBlockNameLength) - { - return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4)); - } + => BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4)); /// /// Processes the application header containing the Adobe identifier @@ -849,60 +795,62 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); } - // Read initial marker definitions. + // Read initial marker definitions const int length = 6; stream.Read(this.temp, 0, length); - // We only support 8-bit and 12-bit precision. - if (Array.IndexOf(this.supportedPrecisions, this.temp[0]) == -1) + // 1 byte: Bits/sample precision + byte precision = this.temp[0]; + + // Validate: only 8-bit and 12-bit precisions are supported + if (Array.IndexOf(this.supportedPrecisions, precision) == -1) { JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision supported."); } - this.Precision = this.temp[0]; + // 2 byte: Height + int frameHeight = (this.temp[1] << 8) | this.temp[2]; - this.Frame = new JpegFrame - { - Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, - Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2, - Precision = this.temp[0], - PixelHeight = (this.temp[1] << 8) | this.temp[2], - PixelWidth = (this.temp[3] << 8) | this.temp[4], - ComponentCount = this.temp[5] - }; - - if (this.Frame.PixelWidth == 0 || this.Frame.PixelHeight == 0) + // 2 byte: Width + int frameWidth = (this.temp[3] << 8) | this.temp[4]; + + // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that) + if (frameHeight == 0 || frameWidth == 0) { - JpegThrowHelper.ThrowInvalidImageDimensions(this.Frame.PixelWidth, this.Frame.PixelHeight); + JpegThrowHelper.ThrowInvalidImageDimensions(frameWidth, frameHeight); } - this.ImageSizeInPixels = new Size(this.Frame.PixelWidth, this.Frame.PixelHeight); - this.ComponentCount = this.Frame.ComponentCount; + // 1 byte: Number of components + byte componentCount = this.temp[5]; + this.ColorSpace = this.DeduceJpegColorSpace(componentCount); - this.ColorSpace = this.DeduceJpegColorSpace(); this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); + if (!metadataOnly) { remaining -= length; + // Validate: remaining part must be equal to components * 3 const int componentBytes = 3; - if (remaining > this.ComponentCount * componentBytes) + if (remaining != componentCount * componentBytes) { JpegThrowHelper.ThrowBadMarker("SOFn", remaining); } + // components*3 bytes: component data stream.Read(this.temp, 0, remaining); // No need to pool this. They max out at 4 - this.Frame.ComponentIds = new byte[this.ComponentCount]; - this.Frame.ComponentOrder = new byte[this.ComponentCount]; - this.Frame.Components = new JpegComponent[this.ComponentCount]; + this.Frame.ComponentIds = new byte[componentCount]; + this.Frame.ComponentOrder = new byte[componentCount]; + this.Frame.Components = new JpegComponent[componentCount]; int maxH = 0; int maxV = 0; int index = 0; - for (int i = 0; i < this.ComponentCount; i++) + for (int i = 0; i < componentCount; i++) { byte hv = this.temp[index + 1]; int h = (hv >> 4) & 15; @@ -926,13 +874,8 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, index += componentBytes; } - this.Frame.MaxHorizontalFactor = maxH; - this.Frame.MaxVerticalFactor = maxV; - this.Frame.InitComponents(); + this.Frame.Init(maxH, maxV); - this.ImageSizeInMCU = new Size(this.Frame.McusPerLine, this.Frame.McusPerColumn); - - // This can be injected in SOF marker callback this.scanDecoder.InjectFrameData(this.Frame, this); } } @@ -996,8 +939,8 @@ private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int rem i += 17 + codeLengthSum; - this.BuildHuffmanTable( - tableType == 0 ? this.dcHuffmanTables : this.acHuffmanTables, + this.scanDecoder.BuildHuffmanTable( + tableType, tableIndex, codeLengthsSpan, huffmanValuesSpan); @@ -1020,87 +963,101 @@ private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int r JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining); } - this.resetInterval = this.ReadUint16(stream); + this.scanDecoder.ResetInterval = this.ReadUint16(stream); } /// /// Processes the SOS (Start of scan marker). /// - private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationToken cancellationToken) + private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining, CancellationToken cancellationToken) { if (this.Frame is null) { JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); } + // 1 byte: Number of components in scan int selectorsCount = stream.ReadByte(); + + // Validate: 0 < count <= totalComponents + if (selectorsCount == 0 || selectorsCount > this.Frame.ComponentCount) + { + // TODO: extract as separate method? + JpegThrowHelper.ThrowInvalidImageContentException($"Invalid number of components in scan: {selectorsCount}."); + } + + // Validate: marker must contain exactly (4 + selectorsCount*2) bytes + int selectorsBytes = selectorsCount * 2; + if (remaining != 4 + selectorsBytes) + { + JpegThrowHelper.ThrowBadMarker("SOS", remaining); + } + + // selectorsCount*2 bytes: component index + huffman tables indices + stream.Read(this.temp, 0, selectorsBytes); + this.Frame.MultiScan = this.Frame.ComponentCount != selectorsCount; - for (int i = 0; i < selectorsCount; i++) + for (int i = 0; i < selectorsBytes; i += 2) { - int componentIndex = -1; - int selector = stream.ReadByte(); + // 1 byte: Component id + int componentSelectorId = this.temp[i]; + int componentIndex = -1; for (int j = 0; j < this.Frame.ComponentIds.Length; j++) { byte id = this.Frame.ComponentIds[j]; - if (selector == id) + if (componentSelectorId == id) { componentIndex = j; break; } } - if (componentIndex < 0) + // Validate: must be found among registered components + if (componentIndex == -1) { - JpegThrowHelper.ThrowInvalidImageContentException($"Unknown component selector {componentIndex}."); + // TODO: extract as separate method? + JpegThrowHelper.ThrowInvalidImageContentException($"Unknown component id in scan: {componentSelectorId}."); } - ref JpegComponent component = ref this.Frame.Components[componentIndex]; - int tableSpec = stream.ReadByte(); - component.DCHuffmanTableId = tableSpec >> 4; - component.ACHuffmanTableId = tableSpec & 15; - this.Frame.ComponentOrder[i] = (byte)componentIndex; - } + this.Frame.ComponentOrder[i / 2] = (byte)componentIndex; - stream.Read(this.temp, 0, 3); + JpegComponent component = this.Frame.Components[componentIndex]; - int spectralStart = this.temp[0]; - int spectralEnd = this.temp[1]; - int successiveApproximation = this.temp[2]; + // 1 byte: Huffman table selectors. + // 4 bits - dc + // 4 bits - ac + int tableSpec = this.temp[i + 1]; + int dcTableIndex = tableSpec >> 4; + int acTableIndex = tableSpec & 15; - // All the comments below are for separate refactoring PR - // Main reason it's not fixed here is to make this commit less intrusive - - // Huffman tables can be calculated directly in the scan decoder class - this.scanDecoder.DcHuffmanTables = this.dcHuffmanTables; - this.scanDecoder.AcHuffmanTables = this.acHuffmanTables; + // Validate: both must be < 4 + if (dcTableIndex >= 4 || acTableIndex >= 4) + { + // TODO: extract as separate method? + JpegThrowHelper.ThrowInvalidImageContentException($"Invalid huffman table for component:{componentSelectorId}: dc={dcTableIndex}, ac={acTableIndex}"); + } - // This can be injectd in DRI marker callback - this.scanDecoder.ResetInterval = this.resetInterval; + component.DCHuffmanTableId = dcTableIndex; + component.ACHuffmanTableId = acTableIndex; + } - // This can be passed as ParseEntropyCodedData() parameter as it is used only there - this.scanDecoder.ComponentsLength = selectorsCount; + // 3 bytes: Progressive scan decoding data + stream.Read(this.temp, 0, 3); - // This is okay to inject here, might be good to wrap it in a separate struct but not really necessary + int spectralStart = this.temp[0]; this.scanDecoder.SpectralStart = spectralStart; + + int spectralEnd = this.temp[1]; this.scanDecoder.SpectralEnd = spectralEnd; + + int successiveApproximation = this.temp[2]; this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4; this.scanDecoder.SuccessiveLow = successiveApproximation & 15; - this.scanDecoder.ParseEntropyCodedData(); + this.scanDecoder.ParseEntropyCodedData(selectorsCount); } - /// - /// Builds the huffman tables - /// - /// The tables - /// The table index - /// The codelengths - /// The values - [MethodImpl(InliningOptions.ShortMethod)] - private void BuildHuffmanTable(HuffmanTable[] tables, int index, ReadOnlySpan codeLengths, ReadOnlySpan values) - => tables[index] = new HuffmanTable(codeLengths, values); - /// /// Reads a from the stream advancing it by two bytes /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index 304dd93a63..d12240cba3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -87,7 +87,9 @@ public partial class JpegDecoderTests TestImages.Jpeg.Issues.Fuzz.ArgumentException826B, TestImages.Jpeg.Issues.Fuzz.ArgumentException826C, TestImages.Jpeg.Issues.Fuzz.AccessViolationException827, - TestImages.Jpeg.Issues.Fuzz.ExecutionEngineException839 + TestImages.Jpeg.Issues.Fuzz.ExecutionEngineException839, + TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693A, + TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693B }; private static readonly Dictionary CustomToleranceValues = diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index a052ee88a5..674aa6d8f6 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -62,10 +62,7 @@ private static bool SkipTest(ITestImageProvider provider) return !TestEnvironment.Is64BitProcess && largeImagesToSkipOn32Bit.Contains(provider.SourceFileOrDescription); } - public JpegDecoderTests(ITestOutputHelper output) - { - this.Output = output; - } + public JpegDecoderTests(ITestOutputHelper output) => this.Output = output; private ITestOutputHelper Output { get; } @@ -163,7 +160,7 @@ public async Task Identify_IsCancellable() { var cts = new CancellationTokenSource(); - var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); + string file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); using var pausedStream = new PausedStream(file); pausedStream.OnWaiting(s => { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs index a124ec1918..0a4d85344b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs @@ -43,12 +43,12 @@ public void ComponentScalingIsCorrect_1ChannelJpeg() { using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(TestImages.Jpeg.Baseline.Jpeg400)) { - Assert.Equal(1, decoder.ComponentCount); + Assert.Equal(1, decoder.Frame.ComponentCount); Assert.Equal(1, decoder.Components.Length); - Size expectedSizeInBlocks = decoder.ImageSizeInPixels.DivideRoundUp(8); + Size expectedSizeInBlocks = decoder.Frame.PixelSize.DivideRoundUp(8); - Assert.Equal(expectedSizeInBlocks, decoder.ImageSizeInMCU); + Assert.Equal(expectedSizeInBlocks, decoder.Frame.McuSize); var uniform1 = new Size(1, 1); JpegComponent c0 = decoder.Components[0]; @@ -70,7 +70,7 @@ public void PrintComponentData(string imageFile) using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) { sb.AppendLine(imageFile); - sb.AppendLine($"Size:{decoder.ImageSizeInPixels} MCU:{decoder.ImageSizeInMCU}"); + sb.AppendLine($"Size:{decoder.Frame.PixelSize} MCU:{decoder.Frame.McuSize}"); JpegComponent c0 = decoder.Components[0]; JpegComponent c1 = decoder.Components[1]; @@ -106,7 +106,7 @@ public void ComponentScalingIsCorrect_MultiChannelJpeg( using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) { - Assert.Equal(componentCount, decoder.ComponentCount); + Assert.Equal(componentCount, decoder.Frame.ComponentCount); Assert.Equal(componentCount, decoder.Components.Length); JpegComponent c0 = decoder.Components[0]; @@ -115,7 +115,7 @@ public void ComponentScalingIsCorrect_MultiChannelJpeg( var uniform1 = new Size(1, 1); - Size expectedLumaSizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(fLuma); + Size expectedLumaSizeInBlocks = decoder.Frame.McuSize.MultiplyBy(fLuma); Size divisor = fLuma.DivideBy(fChroma); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6d2f65f575..fac8cb4a32 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -261,6 +261,8 @@ public static class Fuzz public const string AccessViolationException827 = "Jpg/issues/fuzz/Issue827-AccessViolationException.jpg"; public const string ExecutionEngineException839 = "Jpg/issues/fuzz/Issue839-ExecutionEngineException.jpg"; public const string AccessViolationException922 = "Jpg/issues/fuzz/Issue922-AccessViolationException.jpg"; + public const string IndexOutOfRangeException1693A = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg"; + public const string IndexOutOfRangeException1693B = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg"; } } diff --git a/tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg b/tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg new file mode 100644 index 0000000000..eb8fb9010a --- /dev/null +++ b/tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbb6acd612cdb09825493d04ec7c6aba8ef2a94cc9a86c6b16218720adfb8f5c +size 58065 diff --git a/tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg b/tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg new file mode 100644 index 0000000000..7dd4285914 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8720a9ccf118c3f55407aa250ee490d583286c7e40c8c62a6f8ca449ca3ddff3 +size 58067