From 83b4e56810e7810cfcca0ca280d9da39177a40a7 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 26 Dec 2016 19:56:11 +0100 Subject: [PATCH] cleanup & docs --- .../Jpg/Components/Decoder/HuffmanTree.cs | 109 +- .../Jpg/Components/Decoder/JpegPixelArea.cs | 124 +- .../Jpg/Components/Decoder/YCbCrImage.cs | 134 +- src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs | 2093 +++++++++-------- .../Formats/Jpg/Utils/CleanPooler.cs | 19 +- 5 files changed, 1274 insertions(+), 1205 deletions(-) diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs index 2c08284a35..12acdfc6cb 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs @@ -8,69 +8,77 @@ namespace ImageSharp.Formats.Jpg using System.Buffers; /// - /// Represents a Huffman tree + /// Represents a Huffman tree /// internal struct HuffmanTree : IDisposable { - public static HuffmanTree[] CreateHuffmanTrees() - { - HuffmanTree[] result = new HuffmanTree[(MaxTc + 1) * (MaxTh + 1)]; - for (int i = 0; i < MaxTc + 1; i++) - { - for (int j = 0; j < MaxTh + 1; j++) - { - result[(i * ThRowSize) + j].Init(); - } - } - return result; - } /// - /// The maximum (inclusive) number of codes in a Huffman tree. + /// The maximum (inclusive) number of codes in a Huffman tree. + /// + public const int MaxNCodes = 256; + + /// + /// The maximum (inclusive) number of bits in a Huffman code. + /// + public const int MaxCodeLength = 16; + + /// + /// The maximum number of Huffman table classes + /// + public const int MaxTc = 1; + + /// + /// The maximum number of Huffman table identifiers + /// + public const int MaxTh = 3; + + /// + /// Row size of the Huffman table /// - internal const int MaxNCodes = 256; + public const int ThRowSize = MaxTh + 1; /// - /// The maximum (inclusive) number of bits in a Huffman code. + /// Number of Hufman Trees in the Huffman table /// - internal const int MaxCodeLength = 16; + public const int NumberOfTrees = (MaxTc + 1) * (MaxTh + 1); /// - /// The log-2 size of the Huffman decoder's look-up table. + /// The log-2 size of the Huffman decoder's look-up table. /// - internal const int LutSize = 8; + public const int LutSize = 8; /// - /// Gets or sets the number of codes in the tree. + /// Gets or sets the number of codes in the tree. /// public int Length; /// - /// Gets the look-up table for the next LutSize bits in the bit-stream. - /// The high 8 bits of the uint16 are the encoded value. The low 8 bits - /// are 1 plus the code length, or 0 if the value is too large to fit in - /// lutSize bits. + /// Gets the look-up table for the next LutSize bits in the bit-stream. + /// The high 8 bits of the uint16 are the encoded value. The low 8 bits + /// are 1 plus the code length, or 0 if the value is too large to fit in + /// lutSize bits. /// public ushort[] Lut; /// - /// Gets the the decoded values, sorted by their encoding. + /// Gets the the decoded values, sorted by their encoding. /// public byte[] Values; /// - /// Gets the array of minimum codes. - /// MinCodes[i] is the minimum code of length i, or -1 if there are no codes of that length. + /// Gets the array of minimum codes. + /// MinCodes[i] is the minimum code of length i, or -1 if there are no codes of that length. /// public int[] MinCodes; /// - /// Gets the array of maximum codes. - /// MaxCodes[i] is the maximum code of length i, or -1 if there are no codes of that length. + /// Gets the array of maximum codes. + /// MaxCodes[i] is the maximum code of length i, or -1 if there are no codes of that length. /// public int[] MaxCodes; /// - /// Gets the array of indices. Indices[i] is the index into Values of MinCodes[i]. + /// Gets the array of indices. Indices[i] is the index into Values of MinCodes[i]. /// public int[] Indices; @@ -81,7 +89,25 @@ namespace ImageSharp.Formats.Jpg private static readonly ArrayPool IntBuffer = ArrayPool.Create(MaxCodeLength, 50); /// - /// Initializes the Huffman tree + /// Creates and initializes an array of instances of size + /// + /// An array of instances representing the Huffman table + public static HuffmanTree[] CreateHuffmanTrees() + { + HuffmanTree[] result = new HuffmanTree[NumberOfTrees]; + for (int i = 0; i < MaxTc + 1; i++) + { + for (int j = 0; j < MaxTh + 1; j++) + { + result[(i * ThRowSize) + j].Init(); + } + } + + return result; + } + + /// + /// Initializes the Huffman tree /// public void Init() { @@ -93,7 +119,7 @@ namespace ImageSharp.Formats.Jpg } /// - /// Disposes the underlying buffers + /// Disposes the underlying buffers /// public void Dispose() { @@ -105,12 +131,15 @@ namespace ImageSharp.Formats.Jpg } /// - /// Internal part of the DHT processor, whatever does it mean + /// Internal part of the DHT processor, whatever does it mean /// /// The decoder instance /// The temporal buffer that holds the data that has been read from stream /// Remaining bits - internal void ProcessDefineHuffmanTablesMarkerLoop(JpegDecoderCore decoder, byte[] defineHuffmanTablesData, ref int remaining) + internal void ProcessDefineHuffmanTablesMarkerLoop( + JpegDecoderCore decoder, + byte[] defineHuffmanTablesData, + ref int remaining) { // Read nCodes and huffman.Valuess (and derive h.Length). // nCodes[i] is the number of codes with code length i. @@ -197,17 +226,5 @@ namespace ImageSharp.Formats.Jpg c <<= 1; } } - - /// - /// The maximum number of Huffman table classes - /// - internal const int MaxTc = 1; - - /// - /// The maximum number of Huffman table identifiers - /// - internal const int MaxTh = 3; - - internal const int ThRowSize = MaxTh + 1; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs index 92d0f34294..00e2b0e13a 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs @@ -1,89 +1,127 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageSharp.Formats.Jpg { - - using System; - using System.Buffers; using System.Runtime.CompilerServices; /// - /// Represents a grayscale image + /// Represents an area of a Jpeg subimage (channel) /// internal struct JpegPixelArea { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct from existing data. /// - /// The width. - /// The height. - public static JpegPixelArea CreatePooled(int width, int height) - { - int size = width * height; - var pixels = CleanPooler.RentCleanArray(size); - return new JpegPixelArea(pixels, width, 0); - } - - public JpegPixelArea(byte[] pixels, int widthOrStride, int offset) + /// The pixel array + /// The stride + /// The offset + public JpegPixelArea(byte[] pixels, int striede, int offset) { - this.Stride = widthOrStride; + this.Stride = striede; this.Pixels = pixels; this.Offset = offset; } - public void ReturnPooled() - { - if (this.Pixels == null) return; - CleanPooler.ReturnArray(this.Pixels); - this.Pixels = null; - } - /// - /// Gets or sets the pixels. + /// Gets the pixels. /// public byte[] Pixels { get; private set; } - public bool Created => this.Pixels != null; + /// + /// Gets a value indicating whether the instance has been initalized. (Is not default(JpegPixelArea)) + /// + public bool IsInitialized => this.Pixels != null; /// - /// Gets or sets the width. + /// Gets or the stride. /// - public int Stride { get; private set; } + public int Stride { get; } /// - /// Gets or sets the offset + /// Gets or the offset. /// - public int Offset { get; private set; } - + public int Offset { get; } + /// - /// Get the subarea that belongs to the Block8x8 defined by block indices + /// Gets a of bytes to the pixel area + /// + public MutableSpan Span => new MutableSpan(this.Pixels, this.Offset); + + /// + /// Returns the pixel at (x, y) + /// + /// The x index + /// The y index + /// The pixel value + public byte this[int x, int y] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return this.Pixels[(y * this.Stride) + x]; + } + } + + /// + /// Creates a new instance of the struct. + /// Pixel array will be taken from a pool, this instance will be the owner of it's pixel data, therefore + /// should be called when the instance is no longer needed. + /// + /// The width. + /// The height. + /// A with pooled data + public static JpegPixelArea CreatePooled(int width, int height) + { + int size = width * height; + var pixels = CleanPooler.RentCleanArray(size); + return new JpegPixelArea(pixels, width, 0); + } + + /// + /// Returns to the pool + /// + public void ReturnPooled() + { + if (this.Pixels == null) + { + return; + } + + CleanPooler.ReturnArray(this.Pixels); + this.Pixels = null; + } + + /// + /// Gets the subarea that belongs to the Block8x8 defined by block indices /// /// The block X index /// The block Y index - /// - public JpegPixelArea GetOffsetedAreaForBlock(int bx, int by) + /// The subarea offseted by block indices + public JpegPixelArea GetOffsetedSubAreaForBlock(int bx, int by) { - int offset = this.Offset + 8 * (by * this.Stride + bx); + int offset = this.Offset + (8 * ((by * this.Stride) + bx)); return new JpegPixelArea(this.Pixels, this.Stride, offset); } - public byte this[int x, int y] => this.Pixels[y * this.Stride + x]; - /// - /// Gets the row offset at the given position + /// Gets the row offset at the given position /// /// The y-coordinate of the image. - /// The + /// The public int GetRowOffset(int y) { return this.Offset + (y * this.Stride); } - public MutableSpan Span => new MutableSpan(this.Pixels, this.Offset); - + /// + /// Load values to the pixel area from the given . + /// Level shift [-128.0, 128.0] floating point color values by +128, clip them to [0, 255], and convert them to + /// values + /// + /// The block holding the color values + /// Temporal block provided by the caller [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void LoadColorsFrom(Block8x8F* block, Block8x8F* temp) { @@ -91,4 +129,4 @@ namespace ImageSharp.Formats.Jpg block->CopyColorsTo(new MutableSpan(this.Pixels, this.Offset), this.Stride, temp); } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs index bd0d58dd28..f842a295c9 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs @@ -2,24 +2,22 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageSharp.Formats.Jpg { using System; - using System.Buffers; /// - /// Represents an image made up of three color components (luminance, blue chroma, red chroma) + /// Represents an image made up of three color components (luminance, blue chroma, red chroma) /// internal class YCbCrImage : IDisposable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The width. /// The height. /// The ratio. - public YCbCrImage(int width, int height, YCbCrSubsampleRatio ratio, int yOffset, int cOffset) + public YCbCrImage(int width, int height, YCbCrSubsampleRatio ratio) { int cw, ch; YCbCrSize(width, height, ratio, out cw, out ch); @@ -27,134 +25,120 @@ namespace ImageSharp.Formats.Jpg this.CbPixels = CleanPooler.RentCleanArray(cw * ch); this.CrPixels = CleanPooler.RentCleanArray(cw * ch); this.Ratio = ratio; - this.YOffset = yOffset; - this.COffset = cOffset; + this.YOffset = 0; + this.COffset = 0; this.YStride = width; this.CStride = cw; - this.X = 0; - this.Y = 0; } - public JpegPixelArea YChannel => new JpegPixelArea(this.YPixels, this.YStride, this.YOffset); - - public JpegPixelArea CbChannel => new JpegPixelArea(this.CbPixels, this.CStride, this.COffset); - - public JpegPixelArea CrChannel => new JpegPixelArea(this.CrPixels, this.CStride, this.COffset); - - /// - /// Prevents a default instance of the class from being created. - /// - private YCbCrImage(int yOffset, int cOffset) - { - this.YOffset = yOffset; - this.COffset = cOffset; - } - /// - /// Provides enumeration of the various available subsample ratios. + /// Provides enumeration of the various available subsample ratios. /// public enum YCbCrSubsampleRatio { /// - /// YCbCrSubsampleRatio444 + /// YCbCrSubsampleRatio444 /// YCbCrSubsampleRatio444, /// - /// YCbCrSubsampleRatio422 + /// YCbCrSubsampleRatio422 /// YCbCrSubsampleRatio422, /// - /// YCbCrSubsampleRatio420 + /// YCbCrSubsampleRatio420 /// YCbCrSubsampleRatio420, /// - /// YCbCrSubsampleRatio440 + /// YCbCrSubsampleRatio440 /// YCbCrSubsampleRatio440, /// - /// YCbCrSubsampleRatio411 + /// YCbCrSubsampleRatio411 /// YCbCrSubsampleRatio411, /// - /// YCbCrSubsampleRatio410 + /// YCbCrSubsampleRatio410 /// YCbCrSubsampleRatio410, } /// - /// Gets or sets the luminance components channel. + /// Gets an offseted to the Cb channel /// - public byte[] YPixels { get; } + public JpegPixelArea CbChannel => new JpegPixelArea(this.CbPixels, this.CStride, this.COffset); /// - /// Gets or sets the blue chroma components channel. + /// Gets the blue chroma components channel. /// public byte[] CbPixels { get; } /// - /// Gets or sets the red chroma components channel. + /// Gets the index of the first element of red or blue chroma. /// - public byte[] CrPixels { get; } + public int COffset { get; } /// - /// Gets or sets the Y slice index delta between vertically adjacent pixels. + /// Gets an offseted to the Cr channel /// - public int YStride { get; } + public JpegPixelArea CrChannel => new JpegPixelArea(this.CrPixels, this.CStride, this.COffset); /// - /// Gets or sets the red and blue chroma slice index delta between vertically adjacent pixels - /// that map to separate chroma samples. + /// Gets the red chroma components channel. + /// + public byte[] CrPixels { get; } + + /// + /// Gets the red and blue chroma slice index delta between vertically adjacent pixels + /// that map to separate chroma samples. /// public int CStride { get; } /// - /// Gets or sets the index of the first luminance element. + /// Gets or sets the subsampling ratio. /// - public int YOffset { get; } + public YCbCrSubsampleRatio Ratio { get; set; } /// - /// Gets or sets the index of the first element of red or blue chroma. + /// Gets an offseted to the Y channel /// - public int COffset { get; } + public JpegPixelArea YChannel => new JpegPixelArea(this.YPixels, this.YStride, this.YOffset); /// - /// Gets or sets the horizontal position. + /// Gets the index of the first luminance element. /// - public int X { get; set; } + public int YOffset { get; } /// - /// Gets or sets the vertical position. + /// Gets the luminance components channel. /// - public int Y { get; set; } - + public byte[] YPixels { get; } + /// - /// Gets or sets the subsampling ratio. + /// Gets the Y slice index delta between vertically adjacent pixels. /// - public YCbCrSubsampleRatio Ratio { get; set; } + public int YStride { get; } /// - /// Returns the offset of the first luminance component at the given row + /// Disposes the returning rented arrays to the pools. /// - /// The row number. - /// - /// The . - /// - public int GetRowYOffset(int y) + public void Dispose() { - return y * this.YStride; + CleanPooler.ReturnArray(this.YPixels); + CleanPooler.ReturnArray(this.CrPixels); + CleanPooler.ReturnArray(this.CbPixels); } /// - /// Returns the offset of the first chroma component at the given row + /// Returns the offset of the first chroma component at the given row /// /// The row number. /// - /// The . + /// The . /// public int GetRowCOffset(int y) { @@ -176,14 +160,31 @@ namespace ImageSharp.Formats.Jpg } /// - /// Returns the height and width of the chroma components + /// Returns the offset of the first luminance component at the given row + /// + /// The row number. + /// + /// The . + /// + public int GetRowYOffset(int y) + { + return y * this.YStride; + } + + /// + /// Returns the height and width of the chroma components /// /// The width. /// The height. /// The subsampling ratio. /// The chroma width. /// The chroma height. - private static void YCbCrSize(int width, int height, YCbCrSubsampleRatio ratio, out int chromaWidth, out int chromaHeight) + private static void YCbCrSize( + int width, + int height, + YCbCrSubsampleRatio ratio, + out int chromaWidth, + out int chromaHeight) { switch (ratio) { @@ -215,12 +216,5 @@ namespace ImageSharp.Formats.Jpg break; } } - - public void Dispose() - { - CleanPooler.ReturnArray(this.YPixels); - CleanPooler.ReturnArray(this.CrPixels); - CleanPooler.ReturnArray(this.CbPixels); - } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index 9e5990d2df..1696d0811a 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -2,153 +2,153 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageSharp.Formats { using System; using System.IO; using System.Runtime.CompilerServices; - using System.Runtime.InteropServices; using System.Threading.Tasks; + using ImageSharp.Formats.Jpg; /// - /// Performs the jpeg decoding operation. + /// Performs the jpeg decoding operation. /// internal unsafe class JpegDecoderCore : IDisposable { /// - /// The maximum number of color components + /// The AC table index /// - private const int MaxComponents = 4; + private const int AcTable = 1; /// - /// The maximum number of quantization tables + /// The DC table index /// - private const int MaxTq = 3; + private const int DcTable = 0; /// - /// The DC table index + /// The maximum number of color components /// - private const int DcTable = 0; + private const int MaxComponents = 4; /// - /// The AC table index + /// The maximum number of quantization tables /// - private const int AcTable = 1; + private const int MaxTq = 3; /// - /// The component array + /// The component array /// private readonly Component[] componentArray; /// - /// Saved state between progressive-mode scans. + /// The huffman trees /// - private readonly Block8x8F[][] progCoeffs; + private readonly HuffmanTree[] huffmanTrees; /// - /// The huffman trees + /// Saved state between progressive-mode scans. /// - private readonly HuffmanTree[] huffmanTrees; + private readonly Block8x8F[][] progCoeffs; /// - /// Quantization tables, in zigzag order. + /// Quantization tables, in zigzag order. /// private readonly Block8x8F[] quantizationTables; /// - /// A temporary buffer for holding pixels + /// A temporary buffer for holding pixels /// - private readonly byte[] temp; // TODO: the usage of this buffer is unclean + need to move it to the stack for performance + private readonly byte[] temp; + + // TODO: the usage of this buffer is unclean + need to move it to the stack for performance /// - /// The byte buffer. + /// The App14 marker color-space /// - private Bytes bytes; + private byte adobeTransform; /// - /// The byte buffer. + /// Whether the image is in CMYK format with an App14 marker /// - private Stream inputStream; + private bool adobeTransformValid; /// - /// Holds the unprocessed bits that have been taken from the byte-stream. + /// Holds the unprocessed bits that have been taken from the byte-stream. /// private Bits bits; - /// - /// The image width - /// - private int imageWidth; + private JpegPixelArea blackImage; + + private int blockIndex; /// - /// The image height + /// The byte buffer. /// - private int imageHeight; + private Bytes bytes; /// - /// The number of color components within the image. + /// The number of color components within the image. /// private int componentCount; /// - /// A grayscale image to decode to. + /// End-of-Band run, specified in section G.1.2.2. /// - private JpegPixelArea grayImage; + private ushort eobRun; /// - /// The full color image to decode to. + /// A grayscale image to decode to. /// - private YCbCrImage ycbcrImage; - - - private JpegPixelArea blackImage; + private JpegPixelArea grayImage; /// - /// The restart interval + /// The horizontal resolution. Calculated if the image has a JFIF header. /// - private int restartInterval; + private short horizontalResolution; /// - /// Whether the image is interlaced (progressive) + /// The image height /// - private bool isProgressive; + private int imageHeight; /// - /// Whether the image has a JFIF header + /// The image width /// - private bool isJfif; + private int imageWidth; /// - /// Whether the image is in CMYK format with an App14 marker + /// The byte buffer. /// - private bool adobeTransformValid; + private Stream inputStream; /// - /// The App14 marker color-space + /// Whether the image has a JFIF header /// - private byte adobeTransform; + private bool isJfif; /// - /// End-of-Band run, specified in section G.1.2.2. + /// Whether the image is interlaced (progressive) /// - private ushort eobRun; + private bool isProgressive; /// - /// The horizontal resolution. Calculated if the image has a JFIF header. + /// The restart interval /// - private short horizontalResolution; + private int restartInterval; /// - /// The vertical resolution. Calculated if the image has a JFIF header. + /// The vertical resolution. Calculated if the image has a JFIF header. /// private short verticalResolution; - private int blockIndex; + /// + /// The full color image to decode to. + /// + private YCbCrImage ycbcrImage; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public JpegDecoderCore() { @@ -162,24 +162,24 @@ namespace ImageSharp.Formats } /// - /// ReadByteStuffedByte was throwing exceptions on normal execution path (very inefficent) - /// It's better tho have an error code for this! + /// ReadByteStuffedByte was throwing exceptions on normal execution path (very inefficent) + /// It's better tho have an error code for this! /// internal enum ErrorCodes { /// - /// NoError + /// NoError /// NoError, /// - /// MissingFF00 + /// MissingFF00 /// MissingFF00 } /// - /// Gets or sets the byte buffer. + /// Gets or sets the byte buffer. /// public Bytes Bytes { @@ -195,7 +195,7 @@ namespace ImageSharp.Formats } /// - /// Gets the input stream. + /// Gets the input stream. /// public Stream InputStream { @@ -206,8 +206,8 @@ namespace ImageSharp.Formats } /// - /// Decodes the image from the specified this._stream and sets - /// the data to image. + /// Decodes the image from the specified this._stream and sets + /// the data to image. /// /// The pixel format. /// The image, where the data should be set to. @@ -375,7 +375,7 @@ namespace ImageSharp.Formats } } - if (this.grayImage.Created) + if (this.grayImage.IsInitialized) { this.ConvertFromGrayScale(this.imageWidth, this.imageHeight, image); } @@ -385,7 +385,8 @@ namespace ImageSharp.Formats { if (!this.adobeTransformValid) { - throw new ImageFormatException("Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + throw new ImageFormatException( + "Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); } // See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe @@ -425,7 +426,7 @@ namespace ImageSharp.Formats } /// - /// Dispose + /// Dispose /// public void Dispose() { @@ -441,9 +442,9 @@ namespace ImageSharp.Formats } /// - /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. + /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. /// - /// The + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] internal byte ReadByte() { @@ -451,8 +452,47 @@ namespace ImageSharp.Formats } /// - /// Optimized method to pack bytes to the image from the YCbCr color space. - /// This is faster than implicit casting as it avoids double packing. + /// Reads exactly length bytes into data. It does not care about byte stuffing. + /// + /// The data to write to. + /// The offset in the source buffer + /// The number of bytes to read + internal void ReadFull(byte[] data, int offset, int length) + { + // Unread the overshot bytes, if any. + if (this.bytes.UnreadableBytes != 0) + { + if (this.bits.UnreadBits >= 8) + { + this.UnreadByteStuffedByte(); + } + + this.bytes.UnreadableBytes = 0; + } + + while (length > 0) + { + if (this.bytes.J - this.bytes.I >= length) + { + Array.Copy(this.bytes.Buffer, this.bytes.I, data, offset, length); + this.bytes.I += length; + length -= length; + } + else + { + Array.Copy(this.bytes.Buffer, this.bytes.I, data, offset, this.bytes.J - this.bytes.I); + offset += this.bytes.J - this.bytes.I; + length -= this.bytes.J - this.bytes.I; + this.bytes.I += this.bytes.J - this.bytes.I; + + this.bytes.Fill(this.inputStream); + } + } + } + + /// + /// Optimized method to pack bytes to the image from the YCbCr color space. + /// This is faster than implicit casting as it avoids double packing. /// /// The pixel format. /// The packed pixel. @@ -474,440 +514,794 @@ namespace ImageSharp.Formats } /// - /// Processes a Define Huffman Table marker, and initializes a huffman - /// struct from its contents. Specified in section B.2.4.2. + /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header. /// - /// The remaining bytes in the segment block. - private void ProcessDefineHuffmanTablesMarker(int remaining) + /// The pixel format. + /// The image to assign the resolution to. + private void AssignResolution(Image image) + where TColor : struct, IPackedPixel, IEquatable { - while (remaining > 0) + if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) { - if (remaining < 17) - { - throw new ImageFormatException("DHT has wrong length"); - } - - this.ReadFull(this.temp, 0, 17); - - int tc = this.temp[0] >> 4; - if (tc > HuffmanTree.MaxTc) - { - throw new ImageFormatException("Bad Tc value"); - } - - int th = this.temp[0] & 0x0f; - if (th > HuffmanTree.MaxTh || (!this.isProgressive && (th > 1))) - { - throw new ImageFormatException("Bad Th value"); - } - - int huffTreeIndex = (tc * HuffmanTree.ThRowSize) + th; - this.huffmanTrees[huffTreeIndex].ProcessDefineHuffmanTablesMarkerLoop(this, this.temp, ref remaining); + image.HorizontalResolution = this.horizontalResolution; + image.VerticalResolution = this.verticalResolution; } } - /// - /// Returns the next Huffman-coded value from the bit-stream, decoded according to the given value. + /// Converts the image from the original CMYK image pixels. /// - /// The huffman value - /// The - private byte DecodeHuffman(ref HuffmanTree huffmanTree) + /// The pixel format. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromCmyk(int width, int height, Image image) + where TColor : struct, IPackedPixel, IEquatable { - // Copy stuff to the stack: - if (huffmanTree.Length == 0) - { - throw new ImageFormatException("Uninitialized Huffman table"); - } - - if (this.bits.UnreadBits < 8) - { - ErrorCodes errorCode = this.bits.EnsureNBits(8, this); - - if (errorCode == ErrorCodes.NoError) - { - ushort v = huffmanTree.Lut[(this.bits.Accumulator >> (this.bits.UnreadBits - HuffmanTree.LutSize)) & 0xff]; + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - if (v != 0) - { - byte n = (byte)((v & 0xff) - 1); - this.bits.UnreadBits -= n; - this.bits.Mask >>= n; - return (byte)(v >> 8); - } - } - else - { - this.UnreadByteStuffedByte(); - } - } + image.InitPixels(width, height); - int code = 0; - for (int i = 0; i < HuffmanTree.MaxCodeLength; i++) + using (PixelAccessor pixels = image.Lock()) { - if (this.bits.UnreadBits == 0) - { - ErrorCodes errorCode = this.bits.EnsureNBits(1, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } - } - - if ((this.bits.Accumulator & this.bits.Mask) != 0) - { - code |= 1; - } - - this.bits.UnreadBits--; - this.bits.Mask >>= 1; + Parallel.For( + 0, + height, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); - if (code <= huffmanTree.MaxCodes[i]) - { - return huffmanTree.Values[huffmanTree.Indices[i] + code - huffmanTree.MinCodes[i]]; - } + for (int x = 0; x < width; x++) + { + byte cyan = this.ycbcrImage.YPixels[yo + x]; + byte magenta = this.ycbcrImage.CbPixels[co + (x / scale)]; + byte yellow = this.ycbcrImage.CrPixels[co + (x / scale)]; - code <<= 1; + TColor packed = default(TColor); + this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); + pixels[x, y] = packed; + } + }); } - throw new ImageFormatException("Bad Huffman code"); + this.AssignResolution(image); } /// - /// Decodes a single bit + /// Converts the image from the original grayscale image pixels. /// - /// The - private bool DecodeBit() + /// The pixel format. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromGrayScale(int width, int height, Image image) + where TColor : struct, IPackedPixel, IEquatable { - if (this.bits.UnreadBits == 0) + image.InitPixels(width, height); + + using (PixelAccessor pixels = image.Lock()) { - ErrorCodes errorCode = this.bits.EnsureNBits(1, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } + Parallel.For( + 0, + height, + Bootstrapper.ParallelOptions, + y => + { + int yoff = this.grayImage.GetRowOffset(y); + for (int x = 0; x < width; x++) + { + byte rgb = this.grayImage.Pixels[yoff + x]; + + TColor packed = default(TColor); + packed.PackFromBytes(rgb, rgb, rgb, 255); + pixels[x, y] = packed; + } + }); } - bool ret = (this.bits.Accumulator & this.bits.Mask) != 0; - this.bits.UnreadBits--; - this.bits.Mask >>= 1; - return ret; + this.AssignResolution(image); } /// - /// Decodes the given number of bits + /// Converts the image from the original RBG image pixels. /// - /// The number of bits to decode. - /// The - private uint DecodeBits(int count) + /// The pixel format. + /// The image width. + /// The height. + /// The image. + private void ConvertFromRGB(int width, int height, Image image) + where TColor : struct, IPackedPixel, IEquatable { - if (this.bits.UnreadBits < count) - { - ErrorCodes errorCode = this.bits.EnsureNBits(count, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } - } + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + image.InitPixels(width, height); - uint ret = this.bits.Accumulator >> (this.bits.UnreadBits - count); - ret = (uint)(ret & ((1 << count) - 1)); - this.bits.UnreadBits -= count; - this.bits.Mask >>= count; - return ret; - } + using (PixelAccessor pixels = image.Lock()) + { + Parallel.For( + 0, + height, + Bootstrapper.ParallelOptions, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) + { + byte red = this.ycbcrImage.YPixels[yo + x]; + byte green = this.ycbcrImage.CbPixels[co + (x / scale)]; + byte blue = this.ycbcrImage.CrPixels[co + (x / scale)]; + + TColor packed = default(TColor); + packed.PackFromBytes(red, green, blue, 255); + pixels[x, y] = packed; + } + }); + } + + this.AssignResolution(image); + } /// - /// Undoes the most recent ReadByteStuffedByte call, - /// giving a byte of data back from bits to bytes. The Huffman look-up table - /// requires at least 8 bits for look-up, which means that Huffman decoding can - /// sometimes overshoot and read one or two too many bytes. Two-byte overshoot - /// can happen when expecting to read a 0xff 0x00 byte-stuffed byte. + /// Converts the image from the original YCbCr image pixels. /// - private void UnreadByteStuffedByte() + /// The pixel format. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromYCbCr(int width, int height, Image image) + where TColor : struct, IPackedPixel, IEquatable { - this.bytes.I -= this.bytes.UnreadableBytes; - this.bytes.UnreadableBytes = 0; - if (this.bits.UnreadBits >= 8) + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + image.InitPixels(width, height); + + using (PixelAccessor pixels = image.Lock()) { - this.bits.Accumulator >>= 8; - this.bits.UnreadBits -= 8; - this.bits.Mask >>= 8; + Parallel.For( + 0, + height, + Bootstrapper.ParallelOptions, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YPixels[yo + x]; + byte cb = this.ycbcrImage.CbPixels[co + (x / scale)]; + byte cr = this.ycbcrImage.CrPixels[co + (x / scale)]; + + TColor packed = default(TColor); + PackYcbCr(ref packed, yy, cb, cr); + pixels[x, y] = packed; + } + }); } + + this.AssignResolution(image); } /// - /// Reads exactly length bytes into data. It does not care about byte stuffing. + /// Converts the image from the original YCCK image pixels. /// - /// The data to write to. - /// The offset in the source buffer - /// The number of bytes to read - internal void ReadFull(byte[] data, int offset, int length) + /// The pixel format. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromYcck(int width, int height, Image image) + where TColor : struct, IPackedPixel, IEquatable { - // Unread the overshot bytes, if any. - if (this.bytes.UnreadableBytes != 0) - { - if (this.bits.UnreadBits >= 8) - { - this.UnreadByteStuffedByte(); - } + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - this.bytes.UnreadableBytes = 0; - } + image.InitPixels(width, height); - while (length > 0) + using (PixelAccessor pixels = image.Lock()) { - if (this.bytes.J - this.bytes.I >= length) - { - Array.Copy(this.bytes.Buffer, this.bytes.I, data, offset, length); - this.bytes.I += length; - length -= length; - } - else - { - Array.Copy(this.bytes.Buffer, this.bytes.I, data, offset, this.bytes.J - this.bytes.I); - offset += this.bytes.J - this.bytes.I; - length -= this.bytes.J - this.bytes.I; - this.bytes.I += this.bytes.J - this.bytes.I; + Parallel.For( + 0, + height, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); - this.bytes.Fill(this.inputStream); - } + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YPixels[yo + x]; + byte cb = this.ycbcrImage.CbPixels[co + (x / scale)]; + byte cr = this.ycbcrImage.CrPixels[co + (x / scale)]; + + TColor packed = default(TColor); + this.PackYcck(ref packed, yy, cb, cr, x, y); + pixels[x, y] = packed; + } + }); } + + this.AssignResolution(image); } /// - /// Skips the next n bytes. + /// Decodes a single bit /// - /// The number of bytes to ignore. - private void Skip(int count) + /// The + private bool DecodeBit() { - // Unread the overshot bytes, if any. - if (this.bytes.UnreadableBytes != 0) + if (this.bits.UnreadBits == 0) { - if (this.bits.UnreadBits >= 8) + ErrorCodes errorCode = this.bits.EnsureNBits(1, this); + if (errorCode != ErrorCodes.NoError) { - this.UnreadByteStuffedByte(); + throw new MissingFF00Exception(); } - - this.bytes.UnreadableBytes = 0; } - while (true) - { - int m = this.bytes.J - this.bytes.I; - if (m > count) - { - m = count; - } + bool ret = (this.bits.Accumulator & this.bits.Mask) != 0; + this.bits.UnreadBits--; + this.bits.Mask >>= 1; + return ret; + } - this.bytes.I += m; - count -= m; - if (count == 0) + /// + /// Decodes the given number of bits + /// + /// The number of bits to decode. + /// The + private uint DecodeBits(int count) + { + if (this.bits.UnreadBits < count) + { + ErrorCodes errorCode = this.bits.EnsureNBits(count, this); + if (errorCode != ErrorCodes.NoError) { - break; + throw new MissingFF00Exception(); } - - this.bytes.Fill(this.inputStream); } + + uint ret = this.bits.Accumulator >> (this.bits.UnreadBits - count); + ret = (uint)(ret & ((1 << count) - 1)); + this.bits.UnreadBits -= count; + this.bits.Mask >>= count; + return ret; } /// - /// Processes the Start of Frame marker. Specified in section B.2.2. + /// Returns the next Huffman-coded value from the bit-stream, decoded according to the given value. /// - /// The remaining bytes in the segment block. - private void ProcessStartOfFrameMarker(int remaining) + /// The huffman value + /// The + private byte DecodeHuffman(ref HuffmanTree huffmanTree) { - if (this.componentCount != 0) + // Copy stuff to the stack: + if (huffmanTree.Length == 0) { - throw new ImageFormatException("Multiple SOF markers"); + throw new ImageFormatException("Uninitialized Huffman table"); } - switch (remaining) + if (this.bits.UnreadBits < 8) { - case 6 + (3 * 1): // Grayscale image. - this.componentCount = 1; - break; - case 6 + (3 * 3): // YCbCr or RGB image. - this.componentCount = 3; - break; - case 6 + (3 * 4): // YCbCrK or CMYK image. - this.componentCount = 4; - break; - default: - throw new ImageFormatException("Incorrect number of components"); - } - - this.ReadFull(this.temp, 0, remaining); + ErrorCodes errorCode = this.bits.EnsureNBits(8, this); - // We only support 8-bit precision. - if (this.temp[0] != 8) - { - throw new ImageFormatException("Only 8-Bit precision supported."); - } + if (errorCode == ErrorCodes.NoError) + { + ushort v = + huffmanTree.Lut[(this.bits.Accumulator >> (this.bits.UnreadBits - HuffmanTree.LutSize)) & 0xff]; - this.imageHeight = (this.temp[1] << 8) + this.temp[2]; - this.imageWidth = (this.temp[3] << 8) + this.temp[4]; - if (this.temp[5] != this.componentCount) - { - throw new ImageFormatException("SOF has wrong length"); + if (v != 0) + { + byte n = (byte)((v & 0xff) - 1); + this.bits.UnreadBits -= n; + this.bits.Mask >>= n; + return (byte)(v >> 8); + } + } + else + { + this.UnreadByteStuffedByte(); + } } - for (int i = 0; i < this.componentCount; i++) + int code = 0; + for (int i = 0; i < HuffmanTree.MaxCodeLength; i++) { - this.componentArray[i].Identifier = this.temp[6 + (3 * i)]; - - // Section B.2.2 states that "the value of C_i shall be different from - // the values of C_1 through C_(i-1)". - for (int j = 0; j < i; j++) + if (this.bits.UnreadBits == 0) { - if (this.componentArray[i].Identifier == this.componentArray[j].Identifier) + ErrorCodes errorCode = this.bits.EnsureNBits(1, this); + if (errorCode != ErrorCodes.NoError) { - throw new ImageFormatException("Repeated component identifier"); + throw new MissingFF00Exception(); } } - this.componentArray[i].Selector = this.temp[8 + (3 * i)]; - if (this.componentArray[i].Selector > MaxTq) + if ((this.bits.Accumulator & this.bits.Mask) != 0) { - throw new ImageFormatException("Bad Tq value"); + code |= 1; } - byte hv = this.temp[7 + (3 * i)]; - int h = hv >> 4; - int v = hv & 0x0f; - if (h < 1 || h > 4 || v < 1 || v > 4) - { - throw new ImageFormatException("Unsupported Luma/chroma subsampling ratio"); - } + this.bits.UnreadBits--; + this.bits.Mask >>= 1; - if (h == 3 || v == 3) + if (code <= huffmanTree.MaxCodes[i]) { - throw new ImageFormatException("Lnsupported subsampling ratio"); + return huffmanTree.Values[huffmanTree.Indices[i] + code - huffmanTree.MinCodes[i]]; } - switch (this.componentCount) - { - case 1: + code <<= 1; + } - // If a JPEG image has only one component, section A.2 says "this data - // is non-interleaved by definition" and section A.2.2 says "[in this - // case...] the order of data units within a scan shall be left-to-right - // and top-to-bottom... regardless of the values of H_1 and V_1". Section - // 4.8.2 also says "[for non-interleaved data], the MCU is defined to be - // one data unit". Similarly, section A.1.1 explains that it is the ratio - // of H_i to max_j(H_j) that matters, and similarly for V. For grayscale - // images, H_1 is the maximum H_j for all components j, so that ratio is - // always 1. The component's (h, v) is effectively always (1, 1): even if - // the nominal (h, v) is (2, 1), a 20x5 image is encoded in three 8x8 - // MCUs, not two 16x8 MCUs. - h = 1; - v = 1; + throw new ImageFormatException("Bad Huffman code"); + } + + private JpegPixelArea GetDestinationChannel(int compIndex) + { + if (this.componentCount == 1) + { + return this.grayImage; + } + else + { + switch (compIndex) + { + case 0: + return this.ycbcrImage.YChannel; + case 1: + return this.ycbcrImage.CbChannel; + case 2: + return this.ycbcrImage.CrChannel; + case 3: + return this.blackImage; + default: + throw new ImageFormatException("Too many components"); + } + } + } + + /// + /// Returns a value indicating whether the image in an RGB image. + /// + /// + /// The . + /// + private bool IsRGB() + { + if (this.isJfif) + { + return false; + } + + if (this.adobeTransformValid && this.adobeTransform == JpegConstants.Adobe.ColorTransformUnknown) + { + // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe + // says that 0 means Unknown (and in practice RGB) and 1 means YCbCr. + return true; + } + + return this.componentArray[0].Identifier == 'R' && this.componentArray[1].Identifier == 'G' + && this.componentArray[2].Identifier == 'B'; + } + + /// + /// Makes the image from the buffer. + /// + /// The horizontal MCU count + /// The vertical MCU count + private void MakeImage(int mxx, int myy) + { + if (this.grayImage.IsInitialized || this.ycbcrImage != null) + { + return; + } + + if (this.componentCount == 1) + { + this.grayImage = JpegPixelArea.CreatePooled(8 * mxx, 8 * myy); + } + else + { + int h0 = this.componentArray[0].HorizontalFactor; + int v0 = this.componentArray[0].VerticalFactor; + int horizontalRatio = h0 / this.componentArray[1].HorizontalFactor; + int verticalRatio = v0 / this.componentArray[1].VerticalFactor; + + YCbCrImage.YCbCrSubsampleRatio ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444; + switch ((horizontalRatio << 4) | verticalRatio) + { + case 0x11: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444; + break; + case 0x12: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440; + break; + case 0x21: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422; + break; + case 0x22: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420; + break; + case 0x41: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411; break; + case 0x42: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410; + break; + } + + this.ycbcrImage = new YCbCrImage(8 * h0 * mxx, 8 * v0 * myy, ratio); + + if (this.componentCount == 4) + { + int h3 = this.componentArray[3].HorizontalFactor; + int v3 = this.componentArray[3].VerticalFactor; + + this.blackImage = JpegPixelArea.CreatePooled(8 * h3 * mxx, 8 * v3 * myy); + } + } + } + + /// + /// Optimized method to pack bytes to the image from the CMYK color space. + /// This is faster than implicit casting as it avoids double packing. + /// + /// The pixel format. + /// The packed pixel. + /// The cyan component. + /// The magenta component. + /// The yellow component. + /// The x-position within the image. + /// The y-position within the image. + private void PackCmyk(ref TColor packed, byte c, byte m, byte y, int xx, int yy) + where TColor : struct, IPackedPixel, IEquatable + { + // Get keyline + float keyline = (255 - this.blackImage[xx, yy]) / 255F; + + // Convert back to RGB. CMY are not inverted + byte r = (byte)(((c / 255F) * (1F - keyline)).Clamp(0, 1) * 255); + byte g = (byte)(((m / 255F) * (1F - keyline)).Clamp(0, 1) * 255); + byte b = (byte)(((y / 255F) * (1F - keyline)).Clamp(0, 1) * 255); + + packed.PackFromBytes(r, g, b, 255); + } + + /// + /// Optimized method to pack bytes to the image from the YCCK color space. + /// This is faster than implicit casting as it avoids double packing. + /// + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + /// The x-position within the image. + /// The y-position within the image. + private void PackYcck(ref TColor packed, byte y, byte cb, byte cr, int xx, int yy) + where TColor : struct, IPackedPixel, IEquatable + { + // Convert the YCbCr part of the YCbCrK to RGB, invert the RGB to get + // CMY, and patch in the original K. The RGB to CMY inversion cancels + // out the 'Adobe inversion' described in the applyBlack doc comment + // above, so in practice, only the fourth channel (black) is inverted. + // TODO: We can speed this up further with Vector4 + int ccb = cb - 128; + int ccr = cr - 128; + + // First convert from YCbCr to CMY + float cyan = (y + (1.402F * ccr)).Clamp(0, 255) / 255F; + float magenta = (y - (0.34414F * ccb) - (0.71414F * ccr)).Clamp(0, 255) / 255F; + float yellow = (y + (1.772F * ccb)).Clamp(0, 255) / 255F; + + // Get keyline + float keyline = (255 - this.blackImage[xx, yy]) / 255F; + + // Convert back to RGB + byte r = (byte)(((1 - cyan) * (1 - keyline)).Clamp(0, 1) * 255); + byte g = (byte)(((1 - magenta) * (1 - keyline)).Clamp(0, 1) * 255); + byte b = (byte)(((1 - yellow) * (1 - keyline)).Clamp(0, 1) * 255); + + packed.PackFromBytes(r, g, b, 255); + } + + /// + /// Processes the "Adobe" APP14 segment stores image encoding information for DCT filters. + /// This segment may be copied or deleted as a block using the Extra "Adobe" tag, but note that it is not + /// deleted by default when deleting all metadata because it may affect the appearance of the image. + /// + /// The remaining number of bytes in the stream. + private void ProcessApp14Marker(int remaining) + { + if (remaining < 12) + { + this.Skip(remaining); + return; + } + + this.ReadFull(this.temp, 0, 12); + remaining -= 12; + + if (this.temp[0] == 'A' && this.temp[1] == 'd' && this.temp[2] == 'o' && this.temp[3] == 'b' + && this.temp[4] == 'e') + { + this.adobeTransformValid = true; + this.adobeTransform = this.temp[11]; + } + + if (remaining > 0) + { + this.Skip(remaining); + } + } + + /// + /// Processes the App1 marker retrieving any stored metadata + /// + /// The pixel format. + /// The remaining bytes in the segment block. + /// The image. + private void ProcessApp1Marker(int remaining, Image image) + where TColor : struct, IPackedPixel, IEquatable + { + if (remaining < 6) + { + this.Skip(remaining); + return; + } + + byte[] profile = new byte[remaining]; + this.ReadFull(profile, 0, remaining); + + if (profile[0] == 'E' && profile[1] == 'x' && profile[2] == 'i' && profile[3] == 'f' && profile[4] == '\0' + && profile[5] == '\0') + { + image.ExifProfile = new ExifProfile(profile); + } + } + + /// + /// Processes the application header containing the JFIF identifier plus extra data. + /// + /// The remaining bytes in the segment block. + private void ProcessApplicationHeader(int remaining) + { + if (remaining < 5) + { + this.Skip(remaining); + return; + } + + this.ReadFull(this.temp, 0, 13); + remaining -= 13; + + // TODO: We should be using constants for this. + this.isJfif = this.temp[0] == 'J' && this.temp[1] == 'F' && this.temp[2] == 'I' && this.temp[3] == 'F' + && this.temp[4] == '\x00'; + + if (this.isJfif) + { + this.horizontalResolution = (short)(this.temp[9] + (this.temp[10] << 8)); + this.verticalResolution = (short)(this.temp[11] + (this.temp[12] << 8)); + } + + if (remaining > 0) + { + this.Skip(remaining); + } + } + + private void ProcessBlockImpl( + int ah, + Block8x8F* b, + Block8x8F* temp1, + Block8x8F* temp2, + int* unzigPtr, + Scan[] scan, + int i, + int zigStart, + int zigEnd, + int al, + int[] dc, + int compIndex, + int @by, + int mxx, + int hi, + int bx, + Block8x8F* qt) + { + int huffmannIdx = (AcTable * HuffmanTree.ThRowSize) + scan[i].AcTableSelector; + if (ah != 0) + { + this.Refine(b, ref this.huffmanTrees[huffmannIdx], unzigPtr, zigStart, zigEnd, 1 << al); + } + else + { + int zig = zigStart; + if (zig == 0) + { + zig++; + + // Decode the DC coefficient, as specified in section F.2.2.1. + byte value = + this.DecodeHuffman( + ref this.huffmanTrees[(DcTable * HuffmanTree.ThRowSize) + scan[i].DcTableSelector]); + if (value > 16) + { + throw new ImageFormatException("Excessive DC component"); + } + + int deltaDC = this.bits.ReceiveExtend(value, this); + dc[compIndex] += deltaDC; + + // b[0] = dc[compIndex] << al; + Block8x8F.SetScalarAt(b, 0, dc[compIndex] << al); + } + + if (zig <= zigEnd && this.eobRun > 0) + { + this.eobRun--; + } + else + { + // Decode the AC coefficients, as specified in section F.2.2.2. + // Huffman huffv = ; + for (; zig <= zigEnd; zig++) + { + byte value = this.DecodeHuffman(ref this.huffmanTrees[huffmannIdx]); + byte val0 = (byte)(value >> 4); + byte val1 = (byte)(value & 0x0f); + if (val1 != 0) + { + zig += val0; + if (zig > zigEnd) + { + break; + } - case 3: + int ac = this.bits.ReceiveExtend(val1, this); - // For YCbCr images, we only support 4:4:4, 4:4:0, 4:2:2, 4:2:0, - // 4:1:1 or 4:1:0 chroma subsampling ratios. This implies that the - // (h, v) values for the Y component are either (1, 1), (1, 2), - // (2, 1), (2, 2), (4, 1) or (4, 2), and the Y component's values - // must be a multiple of the Cb and Cr component's values. We also - // assume that the two chroma components have the same subsampling - // ratio. - switch (i) + // b[Unzig[zig]] = ac << al; + Block8x8F.SetScalarAt(b, unzigPtr[zig], ac << al); + } + else { - case 0: + if (val0 != 0x0f) + { + this.eobRun = (ushort)(1 << val0); + if (val0 != 0) { - // Y. - // We have already verified, above, that h and v are both - // either 1, 2 or 4, so invalid (h, v) combinations are those - // with v == 4. - if (v == 4) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } - - break; + this.eobRun |= (ushort)this.DecodeBits(val0); } - case 1: - { - // Cb. - if (this.componentArray[0].HorizontalFactor % h != 0 - || this.componentArray[0].VerticalFactor % v != 0) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } + this.eobRun--; + break; + } - break; - } + zig += 0x0f; + } + } + } + } - case 2: - { - // Cr. - if (this.componentArray[1].HorizontalFactor != h - || this.componentArray[1].VerticalFactor != v) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } + if (this.isProgressive) + { + if (zigEnd != Block8x8F.ScalarCount - 1 || al != 0) + { + // We haven't completely decoded this 8x8 block. Save the coefficients. - break; - } - } + // TODO!!! + // throw new NotImplementedException(); + // this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] = b.Clone(); + this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] = *b; - break; + // At this point, we could execute the rest of the loop body to dequantize and + // perform the inverse DCT, to save early stages of a progressive image to the + // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, + // the jpeg.Decode function does not return until the entire image is decoded, + // so we "continue" here to avoid wasted computation. + return; + } + } - case 4: + // Dequantize, perform the inverse DCT and store the block to the image. + Block8x8F.UnZig(b, qt, unzigPtr); - // For 4-component images (either CMYK or YCbCrK), we only support two - // hv vectors: [0x11 0x11 0x11 0x11] and [0x22 0x11 0x11 0x22]. - // Theoretically, 4-component JPEG images could mix and match hv values - // but in practice, those two combinations are the only ones in use, - // and it simplifies the applyBlack code below if we can assume that: - // - for CMYK, the C and K channels have full samples, and if the M - // and Y channels subsample, they subsample both horizontally and - // vertically. - // - for YCbCrK, the Y and K channels have full samples. - switch (i) - { - case 0: - if (hv != 0x11 && hv != 0x22) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } + DCT.TransformIDCT(ref *b, ref *temp1, ref *temp2); - break; - case 1: - case 2: - if (hv != 0x11) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } + var destChannel = this.GetDestinationChannel(compIndex); + var destArea = destChannel.GetOffsetedSubAreaForBlock(bx, by); + destArea.LoadColorsFrom(temp1, temp2); + } - break; - case 3: - if (this.componentArray[0].HorizontalFactor != h - || this.componentArray[0].VerticalFactor != v) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } + private void ProcessComponentImpl( + int i, + ref Scan currentScan, + Scan[] scan, + ref int totalHv, + ref Component currentComponent) + { + // Section B.2.3 states that "the value of Cs_j shall be different from + // the values of Cs_1 through Cs_(j-1)". Since we have previously + // verified that a frame's component identifiers (C_i values in section + // B.2.2) are unique, it suffices to check that the implicit indexes + // into comp are unique. + for (int j = 0; j < i; j++) + { + if (currentScan.Index == scan[j].Index) + { + throw new ImageFormatException("Repeated component selector"); + } + } - break; - } + totalHv += currentComponent.HorizontalFactor * currentComponent.VerticalFactor; - break; + currentScan.DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); + if (currentScan.DcTableSelector > HuffmanTree.MaxTh) + { + throw new ImageFormatException("Bad DC table selector value"); + } + + currentScan.AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); + if (currentScan.AcTableSelector > HuffmanTree.MaxTh) + { + throw new ImageFormatException("Bad AC table selector value"); + } + } + + /// + /// Processes a Define Huffman Table marker, and initializes a huffman + /// struct from its contents. Specified in section B.2.4.2. + /// + /// The remaining bytes in the segment block. + private void ProcessDefineHuffmanTablesMarker(int remaining) + { + while (remaining > 0) + { + if (remaining < 17) + { + throw new ImageFormatException("DHT has wrong length"); } - this.componentArray[i].HorizontalFactor = h; - this.componentArray[i].VerticalFactor = v; + this.ReadFull(this.temp, 0, 17); + + int tc = this.temp[0] >> 4; + if (tc > HuffmanTree.MaxTc) + { + throw new ImageFormatException("Bad Tc value"); + } + + int th = this.temp[0] & 0x0f; + if (th > HuffmanTree.MaxTh || (!this.isProgressive && (th > 1))) + { + throw new ImageFormatException("Bad Th value"); + } + + int huffTreeIndex = (tc * HuffmanTree.ThRowSize) + th; + this.huffmanTrees[huffTreeIndex].ProcessDefineHuffmanTablesMarkerLoop(this, this.temp, ref remaining); + } + } + + /// + /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in + /// macroblocks + /// + /// The remaining bytes in the segment block. + private void ProcessDefineRestartIntervalMarker(int remaining) + { + if (remaining != 2) + { + throw new ImageFormatException("DRI has wrong length"); } + + this.ReadFull(this.temp, 0, 2); + this.restartInterval = ((int)this.temp[0] << 8) + (int)this.temp[1]; } /// - /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. + /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. /// /// The remaining bytes in the segment block. /// - /// Thrown if the tables do not match the header + /// Thrown if the tables do not match the header /// private void ProcessDqt(int remaining) { @@ -973,327 +1367,231 @@ namespace ImageSharp.Formats } } - /// - /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in macroblocks - /// - /// The remaining bytes in the segment block. - private void ProcessDefineRestartIntervalMarker(int remaining) - { - if (remaining != 2) - { - throw new ImageFormatException("DRI has wrong length"); - } - - this.ReadFull(this.temp, 0, 2); - this.restartInterval = ((int)this.temp[0] << 8) + (int)this.temp[1]; - } - - /// - /// Processes the application header containing the JFIF identifier plus extra data. - /// - /// The remaining bytes in the segment block. - private void ProcessApplicationHeader(int remaining) - { - if (remaining < 5) - { - this.Skip(remaining); - return; - } - - this.ReadFull(this.temp, 0, 13); - remaining -= 13; - - // TODO: We should be using constants for this. - this.isJfif = this.temp[0] == 'J' && this.temp[1] == 'F' && this.temp[2] == 'I' && this.temp[3] == 'F' - && this.temp[4] == '\x00'; - - if (this.isJfif) - { - this.horizontalResolution = (short)(this.temp[9] + (this.temp[10] << 8)); - this.verticalResolution = (short)(this.temp[11] + (this.temp[12] << 8)); - } - - if (remaining > 0) - { - this.Skip(remaining); - } - } - - /// - /// Processes the App1 marker retrieving any stored metadata - /// - /// The pixel format. - /// The remaining bytes in the segment block. - /// The image. - private void ProcessApp1Marker(int remaining, Image image) - where TColor : struct, IPackedPixel, IEquatable + private void ProcessScanImpl(int i, ref Scan currentScan, Scan[] scan, ref int totalHv) { - if (remaining < 6) + // Component selector. + int cs = this.temp[1 + (2 * i)]; + int compIndex = -1; + for (int j = 0; j < this.componentCount; j++) { - this.Skip(remaining); - return; + // Component compv = ; + if (cs == this.componentArray[j].Identifier) + { + compIndex = j; + } } - byte[] profile = new byte[remaining]; - this.ReadFull(profile, 0, remaining); - - if (profile[0] == 'E' && profile[1] == 'x' && profile[2] == 'i' && profile[3] == 'f' && profile[4] == '\0' - && profile[5] == '\0') + if (compIndex < 0) { - image.ExifProfile = new ExifProfile(profile); + throw new ImageFormatException("Unknown component selector"); } + + currentScan.Index = (byte)compIndex; + + this.ProcessComponentImpl(i, ref currentScan, scan, ref totalHv, ref this.componentArray[compIndex]); } /// - /// Processes the "Adobe" APP14 segment stores image encoding information for DCT filters. - /// This segment may be copied or deleted as a block using the Extra "Adobe" tag, but note that it is not - /// deleted by default when deleting all metadata because it may affect the appearance of the image. + /// Processes the Start of Frame marker. Specified in section B.2.2. /// - /// The remaining number of bytes in the stream. - private void ProcessApp14Marker(int remaining) + /// The remaining bytes in the segment block. + private void ProcessStartOfFrameMarker(int remaining) { - if (remaining < 12) + if (this.componentCount != 0) { - this.Skip(remaining); - return; + throw new ImageFormatException("Multiple SOF markers"); } - this.ReadFull(this.temp, 0, 12); - remaining -= 12; - - if (this.temp[0] == 'A' && this.temp[1] == 'd' && this.temp[2] == 'o' && this.temp[3] == 'b' - && this.temp[4] == 'e') + switch (remaining) { - this.adobeTransformValid = true; - this.adobeTransform = this.temp[11]; + case 6 + (3 * 1): // Grayscale image. + this.componentCount = 1; + break; + case 6 + (3 * 3): // YCbCr or RGB image. + this.componentCount = 3; + break; + case 6 + (3 * 4): // YCbCrK or CMYK image. + this.componentCount = 4; + break; + default: + throw new ImageFormatException("Incorrect number of components"); } - if (remaining > 0) + this.ReadFull(this.temp, 0, remaining); + + // We only support 8-bit precision. + if (this.temp[0] != 8) { - this.Skip(remaining); + throw new ImageFormatException("Only 8-Bit precision supported."); } - } - /// - /// Converts the image from the original YCCK image pixels. - /// - /// The pixel format. - /// The image width. - /// The image height. - /// The image. - private void ConvertFromYcck(int width, int height, Image image) - where TColor : struct, IPackedPixel, IEquatable - { - int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - - image.InitPixels(width, height); - - using (PixelAccessor pixels = image.Lock()) + this.imageHeight = (this.temp[1] << 8) + this.temp[2]; + this.imageWidth = (this.temp[3] << 8) + this.temp[4]; + if (this.temp[5] != this.componentCount) { - Parallel.For( - 0, - height, - y => - { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) - { - byte yy = this.ycbcrImage.YPixels[yo + x]; - byte cb = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte cr = this.ycbcrImage.CrPixels[co + (x / scale)]; - - TColor packed = default(TColor); - this.PackYcck(ref packed, yy, cb, cr, x, y); - pixels[x, y] = packed; - } - }); + throw new ImageFormatException("SOF has wrong length"); } - this.AssignResolution(image); - } - - /// - /// Converts the image from the original CMYK image pixels. - /// - /// The pixel format. - /// The image width. - /// The image height. - /// The image. - private void ConvertFromCmyk(int width, int height, Image image) - where TColor : struct, IPackedPixel, IEquatable - { - int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - - image.InitPixels(width, height); - - using (PixelAccessor pixels = image.Lock()) + for (int i = 0; i < this.componentCount; i++) { - Parallel.For( - 0, - height, - y => + this.componentArray[i].Identifier = this.temp[6 + (3 * i)]; + + // Section B.2.2 states that "the value of C_i shall be different from + // the values of C_1 through C_(i-1)". + for (int j = 0; j < i; j++) + { + if (this.componentArray[i].Identifier == this.componentArray[j].Identifier) { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); + throw new ImageFormatException("Repeated component identifier"); + } + } - for (int x = 0; x < width; x++) - { - byte cyan = this.ycbcrImage.YPixels[yo + x]; - byte magenta = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte yellow = this.ycbcrImage.CrPixels[co + (x / scale)]; + this.componentArray[i].Selector = this.temp[8 + (3 * i)]; + if (this.componentArray[i].Selector > MaxTq) + { + throw new ImageFormatException("Bad Tq value"); + } - TColor packed = default(TColor); - this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); - pixels[x, y] = packed; - } - }); - } + byte hv = this.temp[7 + (3 * i)]; + int h = hv >> 4; + int v = hv & 0x0f; + if (h < 1 || h > 4 || v < 1 || v > 4) + { + throw new ImageFormatException("Unsupported Luma/chroma subsampling ratio"); + } - this.AssignResolution(image); - } + if (h == 3 || v == 3) + { + throw new ImageFormatException("Lnsupported subsampling ratio"); + } - /// - /// Converts the image from the original grayscale image pixels. - /// - /// The pixel format. - /// The image width. - /// The image height. - /// The image. - private void ConvertFromGrayScale(int width, int height, Image image) - where TColor : struct, IPackedPixel, IEquatable - { - image.InitPixels(width, height); + switch (this.componentCount) + { + case 1: - using (PixelAccessor pixels = image.Lock()) - { - Parallel.For( - 0, - height, - Bootstrapper.ParallelOptions, - y => - { - int yoff = this.grayImage.GetRowOffset(y); - for (int x = 0; x < width; x++) - { - byte rgb = this.grayImage.Pixels[yoff + x]; + // If a JPEG image has only one component, section A.2 says "this data + // is non-interleaved by definition" and section A.2.2 says "[in this + // case...] the order of data units within a scan shall be left-to-right + // and top-to-bottom... regardless of the values of H_1 and V_1". Section + // 4.8.2 also says "[for non-interleaved data], the MCU is defined to be + // one data unit". Similarly, section A.1.1 explains that it is the ratio + // of H_i to max_j(H_j) that matters, and similarly for V. For grayscale + // images, H_1 is the maximum H_j for all components j, so that ratio is + // always 1. The component's (h, v) is effectively always (1, 1): even if + // the nominal (h, v) is (2, 1), a 20x5 image is encoded in three 8x8 + // MCUs, not two 16x8 MCUs. + h = 1; + v = 1; + break; - TColor packed = default(TColor); - packed.PackFromBytes(rgb, rgb, rgb, 255); - pixels[x, y] = packed; - } - }); - } + case 3: - this.AssignResolution(image); - } + // For YCbCr images, we only support 4:4:4, 4:4:0, 4:2:2, 4:2:0, + // 4:1:1 or 4:1:0 chroma subsampling ratios. This implies that the + // (h, v) values for the Y component are either (1, 1), (1, 2), + // (2, 1), (2, 2), (4, 1) or (4, 2), and the Y component's values + // must be a multiple of the Cb and Cr component's values. We also + // assume that the two chroma components have the same subsampling + // ratio. + switch (i) + { + case 0: + { + // Y. + // We have already verified, above, that h and v are both + // either 1, 2 or 4, so invalid (h, v) combinations are those + // with v == 4. + if (v == 4) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - /// - /// Converts the image from the original YCbCr image pixels. - /// - /// The pixel format. - /// The image width. - /// The image height. - /// The image. - private void ConvertFromYCbCr(int width, int height, Image image) - where TColor : struct, IPackedPixel, IEquatable - { - int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - image.InitPixels(width, height); + break; + } - using (PixelAccessor pixels = image.Lock()) - { - Parallel.For( - 0, - height, - Bootstrapper.ParallelOptions, - y => - { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); + case 1: + { + // Cb. + if (this.componentArray[0].HorizontalFactor % h != 0 + || this.componentArray[0].VerticalFactor % v != 0) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - for (int x = 0; x < width; x++) - { - byte yy = this.ycbcrImage.YPixels[yo + x]; - byte cb = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte cr = this.ycbcrImage.CrPixels[co + (x / scale)]; + break; + } - TColor packed = default(TColor); - PackYcbCr(ref packed, yy, cb, cr); - pixels[x, y] = packed; - } - }); - } + case 2: + { + // Cr. + if (this.componentArray[1].HorizontalFactor != h + || this.componentArray[1].VerticalFactor != v) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - this.AssignResolution(image); - } + break; + } + } - /// - /// Converts the image from the original RBG image pixels. - /// - /// The pixel format. - /// The image width. - /// The height. - /// The image. - private void ConvertFromRGB(int width, int height, Image image) - where TColor : struct, IPackedPixel, IEquatable - { - int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - image.InitPixels(width, height); + break; - using (PixelAccessor pixels = image.Lock()) - { - Parallel.For( - 0, - height, - Bootstrapper.ParallelOptions, - y => + case 4: + + // For 4-component images (either CMYK or YCbCrK), we only support two + // hv vectors: [0x11 0x11 0x11 0x11] and [0x22 0x11 0x11 0x22]. + // Theoretically, 4-component JPEG images could mix and match hv values + // but in practice, those two combinations are the only ones in use, + // and it simplifies the applyBlack code below if we can assume that: + // - for CMYK, the C and K channels have full samples, and if the M + // and Y channels subsample, they subsample both horizontally and + // vertically. + // - for YCbCrK, the Y and K channels have full samples. + switch (i) { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); + case 0: + if (hv != 0x11 && hv != 0x22) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - for (int x = 0; x < width; x++) - { - byte red = this.ycbcrImage.YPixels[yo + x]; - byte green = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte blue = this.ycbcrImage.CrPixels[co + (x / scale)]; + break; + case 1: + case 2: + if (hv != 0x11) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - TColor packed = default(TColor); - packed.PackFromBytes(red, green, blue, 255); - pixels[x, y] = packed; - } - }); - } + break; + case 3: + if (this.componentArray[0].HorizontalFactor != h + || this.componentArray[0].VerticalFactor != v) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - this.AssignResolution(image); - } + break; + } - /// - /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header. - /// - /// The pixel format. - /// The image to assign the resolution to. - private void AssignResolution(Image image) - where TColor : struct, IPackedPixel, IEquatable - { - if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) - { - image.HorizontalResolution = this.horizontalResolution; - image.VerticalResolution = this.verticalResolution; + break; + } + + this.componentArray[i].HorizontalFactor = h; + this.componentArray[i].VerticalFactor = v; } } /// - /// Processes the SOS (Start of scan marker). + /// Processes the SOS (Start of scan marker). /// /// - /// TODO: This also needs some significant refactoring to follow a more OO format. + /// TODO: This also needs some significant refactoring to follow a more OO format. /// /// The remaining bytes in the segment block. /// - /// Missing SOF Marker - /// SOS has wrong length + /// Missing SOF Marker + /// SOS has wrong length /// private void ProcessStartOfScan(int remaining) { @@ -1378,7 +1676,7 @@ namespace ImageSharp.Formats int v0 = this.componentArray[0].VerticalFactor; int mxx = (this.imageWidth + (8 * h0) - 1) / (8 * h0); int myy = (this.imageHeight + (8 * v0) - 1) / (8 * v0); - + this.MakeImage(mxx, myy); if (this.isProgressive) @@ -1410,7 +1708,6 @@ namespace ImageSharp.Formats int bx, by, blockCount = 0; // TODO: A DecoderScanProcessor struct could clean up this mess - Block8x8F b = default(Block8x8F); Block8x8F temp1 = default(Block8x8F); Block8x8F temp2 = default(Block8x8F); @@ -1473,7 +1770,7 @@ namespace ImageSharp.Formats int qtIndex = this.componentArray[compIndex].Selector; // TODO: A DecoderScanProcessor struct could clean up this mess - // TODO: Reading & processing blocks should be done in 2 separate loops. The second one could be parallelized. (The first one could be async) + // TODO: Reading & processing blocks should be done in 2 separate loops. The second one could be parallelized. The first one could be async. fixed (Block8x8F* qtp = &this.quantizationTables[qtIndex]) { // Load the previous partially decoded coefficients, if applicable. @@ -1516,264 +1813,59 @@ namespace ImageSharp.Formats i, zigStart, zigEnd, - al, - dc, - compIndex, - @by, - mxx, - hi, - bx, - qtp); - } - } - } - - // for j - } - - // for i - mcu++; - - if (this.restartInterval > 0 && mcu % this.restartInterval == 0 && mcu < mxx * myy) - { - // A more sophisticated decoder could use RST[0-7] markers to resynchronize from corrupt input, - // but this one assumes well-formed input, and hence the restart marker follows immediately. - this.ReadFull(this.temp, 0, 2); - if (this.temp[0] != 0xff || this.temp[1] != expectedRst) - { - throw new ImageFormatException("Bad RST marker"); - } - - expectedRst++; - if (expectedRst == JpegConstants.Markers.RST7 + 1) - { - expectedRst = JpegConstants.Markers.RST0; - } - - // Reset the Huffman decoder. - this.bits = default(Bits); - - // Reset the DC components, as per section F.2.1.3.1. - dc = new int[MaxComponents]; - - // Reset the progressive decoder state, as per section G.1.2.2. - this.eobRun = 0; - } - } - - // for mx - } - - // for my - } - - private void ProcessBlockImpl( - int ah, - Block8x8F* b, - Block8x8F* temp1, - Block8x8F* temp2, - int* unzigPtr, - Scan[] scan, - int i, - int zigStart, - int zigEnd, - int al, - int[] dc, - int compIndex, - int @by, - int mxx, - int hi, - int bx, - Block8x8F* qt) - { - int huffmannIdx = (AcTable * HuffmanTree.ThRowSize) + scan[i].AcTableSelector; - if (ah != 0) - { - this.Refine(b, ref this.huffmanTrees[huffmannIdx], unzigPtr, zigStart, zigEnd, 1 << al); - } - else - { - int zig = zigStart; - if (zig == 0) - { - zig++; - - // Decode the DC coefficient, as specified in section F.2.2.1. - byte value = this.DecodeHuffman( - ref this.huffmanTrees[(DcTable * HuffmanTree.ThRowSize) + scan[i].DcTableSelector]); - if (value > 16) - { - throw new ImageFormatException("Excessive DC component"); - } - - int deltaDC = this.bits.ReceiveExtend(value, this); - dc[compIndex] += deltaDC; - - // b[0] = dc[compIndex] << al; - Block8x8F.SetScalarAt(b, 0, dc[compIndex] << al); - } - - if (zig <= zigEnd && this.eobRun > 0) - { - this.eobRun--; - } - else - { - // Decode the AC coefficients, as specified in section F.2.2.2. - // Huffman huffv = ; - for (; zig <= zigEnd; zig++) - { - byte value = this.DecodeHuffman(ref this.huffmanTrees[huffmannIdx]); - byte val0 = (byte)(value >> 4); - byte val1 = (byte)(value & 0x0f); - if (val1 != 0) - { - zig += val0; - if (zig > zigEnd) - { - break; - } - - int ac = this.bits.ReceiveExtend(val1, this); - - // b[Unzig[zig]] = ac << al; - Block8x8F.SetScalarAt(b, unzigPtr[zig], ac << al); - } - else - { - if (val0 != 0x0f) - { - this.eobRun = (ushort)(1 << val0); - if (val0 != 0) - { - this.eobRun |= (ushort)this.DecodeBits(val0); - } - - this.eobRun--; - break; - } - - zig += 0x0f; - } - } - } - } - - if (this.isProgressive) - { - if (zigEnd != Block8x8F.ScalarCount - 1 || al != 0) - { - // We haven't completely decoded this 8x8 block. Save the coefficients. - - // TODO!!! - // throw new NotImplementedException(); - // this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] = b.Clone(); - this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] = *b; - - // At this point, we could execute the rest of the loop body to dequantize and - // perform the inverse DCT, to save early stages of a progressive image to the - // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, - // the jpeg.Decode function does not return until the entire image is decoded, - // so we "continue" here to avoid wasted computation. - return; - } - } - - // Dequantize, perform the inverse DCT and store the block to the image. - Block8x8F.UnZig(b, qt, unzigPtr); - - DCT.TransformIDCT(ref *b, ref *temp1, ref *temp2); + al, + dc, + compIndex, + @by, + mxx, + hi, + bx, + qtp); + } + } + } - var destChannel = this.GetDestinationChannel(compIndex); - var destArea = destChannel.GetOffsetedAreaForBlock(bx, by); - destArea.LoadColorsFrom(temp1, temp2); - } + // for j + } - private JpegPixelArea GetDestinationChannel(int compIndex) - { - if (this.componentCount == 1) - { - return this.grayImage; - } - else - { - switch (compIndex) - { - case 0: - return this.ycbcrImage.YChannel; - case 1: - return this.ycbcrImage.CbChannel; - case 2: - return this.ycbcrImage.CrChannel; - case 3: - return this.blackImage; - default: - throw new ImageFormatException("Too many components"); - } - } - } + // for i + mcu++; - private void ProcessScanImpl(int i, ref Scan currentScan, Scan[] scan, ref int totalHv) - { - // Component selector. - int cs = this.temp[1 + (2 * i)]; - int compIndex = -1; - for (int j = 0; j < this.componentCount; j++) - { - // Component compv = ; - if (cs == this.componentArray[j].Identifier) - { - compIndex = j; - } - } + if (this.restartInterval > 0 && mcu % this.restartInterval == 0 && mcu < mxx * myy) + { + // A more sophisticated decoder could use RST[0-7] markers to resynchronize from corrupt input, + // but this one assumes well-formed input, and hence the restart marker follows immediately. + this.ReadFull(this.temp, 0, 2); + if (this.temp[0] != 0xff || this.temp[1] != expectedRst) + { + throw new ImageFormatException("Bad RST marker"); + } - if (compIndex < 0) - { - throw new ImageFormatException("Unknown component selector"); - } + expectedRst++; + if (expectedRst == JpegConstants.Markers.RST7 + 1) + { + expectedRst = JpegConstants.Markers.RST0; + } - currentScan.Index = (byte)compIndex; + // Reset the Huffman decoder. + this.bits = default(Bits); - this.ProcessComponentImpl(i, ref currentScan, scan, ref totalHv, ref this.componentArray[compIndex]); - } + // Reset the DC components, as per section F.2.1.3.1. + dc = new int[MaxComponents]; - private void ProcessComponentImpl( - int i, - ref Scan currentScan, - Scan[] scan, - ref int totalHv, - ref Component currentComponent) - { - // Section B.2.3 states that "the value of Cs_j shall be different from - // the values of Cs_1 through Cs_(j-1)". Since we have previously - // verified that a frame's component identifiers (C_i values in section - // B.2.2) are unique, it suffices to check that the implicit indexes - // into comp are unique. - for (int j = 0; j < i; j++) - { - if (currentScan.Index == scan[j].Index) - { - throw new ImageFormatException("Repeated component selector"); + // Reset the progressive decoder state, as per section G.1.2.2. + this.eobRun = 0; + } } - } - totalHv += currentComponent.HorizontalFactor * currentComponent.VerticalFactor; - - currentScan.DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); - if (currentScan.DcTableSelector > HuffmanTree.MaxTh) - { - throw new ImageFormatException("Bad DC table selector value"); + // for mx } - currentScan.AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); - if (currentScan.AcTableSelector > HuffmanTree.MaxTh) - { - throw new ImageFormatException("Bad AC table selector value"); - } + // for my } /// - /// Decodes a successive approximation refinement block, as specified in section G.1.2. + /// Decodes a successive approximation refinement block, as specified in section G.1.2. /// /// The block of coefficients /// The Huffman tree @@ -1875,16 +1967,16 @@ namespace ImageSharp.Formats } /// - /// Refines non-zero entries of b in zig-zag order. - /// If >= 0, the first zero entries are skipped over. + /// Refines non-zero entries of b in zig-zag order. + /// If >= 0, the first zero entries are skipped over. /// /// The block of coefficients /// The zig-zag start index /// The zig-zag end index /// The non-zero entry /// The low transform offset - /// Pointer to the Jpeg Unzig data (data part of ) - /// The + /// Pointer to the Jpeg Unzig data (data part of ) + /// The private int RefineNonZeroes(Block8x8F* b, int zig, int zigEnd, int nz, int delta, int* unzigPtr) { for (; zig <= zigEnd; zig++) @@ -1924,192 +2016,103 @@ namespace ImageSharp.Formats return zig; } - + /// - /// Makes the image from the buffer. + /// Skips the next n bytes. /// - /// The horizontal MCU count - /// The vertical MCU count - private void MakeImage(int mxx, int myy) + /// The number of bytes to ignore. + private void Skip(int count) { - if (this.grayImage.Created || this.ycbcrImage != null) return; - - if (this.componentCount == 1) + // Unread the overshot bytes, if any. + if (this.bytes.UnreadableBytes != 0) { - this.grayImage = JpegPixelArea.CreatePooled(8 * mxx, 8 * myy); - //this.grayImage = gray.Subimage(0, 0, this.imageWidth, this.imageHeight); + if (this.bits.UnreadBits >= 8) + { + this.UnreadByteStuffedByte(); + } + + this.bytes.UnreadableBytes = 0; } - else - { - int h0 = this.componentArray[0].HorizontalFactor; - int v0 = this.componentArray[0].VerticalFactor; - int horizontalRatio = h0 / this.componentArray[1].HorizontalFactor; - int verticalRatio = v0 / this.componentArray[1].VerticalFactor; - YCbCrImage.YCbCrSubsampleRatio ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444; - switch ((horizontalRatio << 4) | verticalRatio) + while (true) + { + int m = this.bytes.J - this.bytes.I; + if (m > count) { - case 0x11: - ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444; - break; - case 0x12: - ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440; - break; - case 0x21: - ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422; - break; - case 0x22: - ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420; - break; - case 0x41: - ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411; - break; - case 0x42: - ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410; - break; + m = count; } - this.ycbcrImage = new YCbCrImage(8 * h0 * mxx, 8 * v0 * myy, ratio, 0, 0); - - if (this.componentCount == 4) + this.bytes.I += m; + count -= m; + if (count == 0) { - int h3 = this.componentArray[3].HorizontalFactor; - int v3 = this.componentArray[3].VerticalFactor; - - this.blackImage = JpegPixelArea.CreatePooled(8 * h3 * mxx, 8 * v3 * myy); - + break; } + + this.bytes.Fill(this.inputStream); } } /// - /// Returns a value indicating whether the image in an RGB image. + /// Undoes the most recent ReadByteStuffedByte call, + /// giving a byte of data back from bits to bytes. The Huffman look-up table + /// requires at least 8 bits for look-up, which means that Huffman decoding can + /// sometimes overshoot and read one or two too many bytes. Two-byte overshoot + /// can happen when expecting to read a 0xff 0x00 byte-stuffed byte. /// - /// - /// The . - /// - private bool IsRGB() + private void UnreadByteStuffedByte() { - if (this.isJfif) - { - return false; - } - - if (this.adobeTransformValid && this.adobeTransform == JpegConstants.Adobe.ColorTransformUnknown) + this.bytes.I -= this.bytes.UnreadableBytes; + this.bytes.UnreadableBytes = 0; + if (this.bits.UnreadBits >= 8) { - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe - // says that 0 means Unknown (and in practice RGB) and 1 means YCbCr. - return true; + this.bits.Accumulator >>= 8; + this.bits.UnreadBits -= 8; + this.bits.Mask >>= 8; } - - return this.componentArray[0].Identifier == 'R' && this.componentArray[1].Identifier == 'G' - && this.componentArray[2].Identifier == 'B'; - } - - /// - /// Optimized method to pack bytes to the image from the YCCK color space. - /// This is faster than implicit casting as it avoids double packing. - /// - /// The pixel format. - /// The packed pixel. - /// The y luminance component. - /// The cb chroma component. - /// The cr chroma component. - /// The x-position within the image. - /// The y-position within the image. - private void PackYcck(ref TColor packed, byte y, byte cb, byte cr, int xx, int yy) - where TColor : struct, IPackedPixel, IEquatable - { - // Convert the YCbCr part of the YCbCrK to RGB, invert the RGB to get - // CMY, and patch in the original K. The RGB to CMY inversion cancels - // out the 'Adobe inversion' described in the applyBlack doc comment - // above, so in practice, only the fourth channel (black) is inverted. - // TODO: We can speed this up further with Vector4 - int ccb = cb - 128; - int ccr = cr - 128; - - // First convert from YCbCr to CMY - float cyan = (y + (1.402F * ccr)).Clamp(0, 255) / 255F; - float magenta = (y - (0.34414F * ccb) - (0.71414F * ccr)).Clamp(0, 255) / 255F; - float yellow = (y + (1.772F * ccb)).Clamp(0, 255) / 255F; - - // Get keyline - float keyline = (255 - this.blackImage[xx, yy]) / 255F; - - // Convert back to RGB - byte r = (byte)(((1 - cyan) * (1 - keyline)).Clamp(0, 1) * 255); - byte g = (byte)(((1 - magenta) * (1 - keyline)).Clamp(0, 1) * 255); - byte b = (byte)(((1 - yellow) * (1 - keyline)).Clamp(0, 1) * 255); - - packed.PackFromBytes(r, g, b, 255); - } - - /// - /// Optimized method to pack bytes to the image from the CMYK color space. - /// This is faster than implicit casting as it avoids double packing. - /// - /// The pixel format. - /// The packed pixel. - /// The cyan component. - /// The magenta component. - /// The yellow component. - /// The x-position within the image. - /// The y-position within the image. - private void PackCmyk(ref TColor packed, byte c, byte m, byte y, int xx, int yy) - where TColor : struct, IPackedPixel, IEquatable - { - // Get keyline - float keyline = (255 - this.blackImage[xx, yy]) / 255F; - - // Convert back to RGB. CMY are not inverted - byte r = (byte)(((c / 255F) * (1F - keyline)).Clamp(0, 1) * 255); - byte g = (byte)(((m / 255F) * (1F - keyline)).Clamp(0, 1) * 255); - byte b = (byte)(((y / 255F) * (1F - keyline)).Clamp(0, 1) * 255); - - packed.PackFromBytes(r, g, b, 255); } /// - /// Represents a component scan + /// Represents a component scan /// private struct Scan { /// - /// Gets or sets the component index. + /// Gets or sets the component index. /// public byte Index { get; set; } /// - /// Gets or sets the DC table selector + /// Gets or sets the DC table selector /// public byte DcTableSelector { get; set; } /// - /// Gets or sets the AC table selector + /// Gets or sets the AC table selector /// public byte AcTableSelector { get; set; } } /// - /// The missing ff00 exception. + /// The EOF (End of File exception). + /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker /// - internal class MissingFF00Exception : Exception + internal class EOFException : Exception { } /// - /// The EOF (End of File exception). - /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker + /// The missing ff00 exception. /// - internal class EOFException : Exception + internal class MissingFF00Exception : Exception { } /// - /// The short huffman data exception. + /// The short huffman data exception. /// private class ShortHuffmanDataException : Exception { } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Utils/CleanPooler.cs b/src/ImageSharp/Formats/Jpg/Utils/CleanPooler.cs index a181eeee0a..7369d658f2 100644 --- a/src/ImageSharp/Formats/Jpg/Utils/CleanPooler.cs +++ b/src/ImageSharp/Formats/Jpg/Utils/CleanPooler.cs @@ -1,13 +1,30 @@ -namespace ImageSharp.Formats +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace ImageSharp.Formats { using System.Buffers; + /// + /// Wraps to always provide arrays initialized with default(T) + /// + /// The element type internal class CleanPooler { private static readonly ArrayPool Pool = ArrayPool.Create(); + /// + /// Rents a clean array + /// + /// The minimum array length + /// A clean array of T public static T[] RentCleanArray(int minimumLength) => Pool.Rent(minimumLength); + /// + /// Retursn array to the pool + /// + /// The array public static void ReturnArray(T[] array) => Pool.Return(array, true); } } \ No newline at end of file