diff --git a/ImageSharp.sln.DotSettings b/ImageSharp.sln.DotSettings index 5acec071a..b0f5aa692 100644 --- a/ImageSharp.sln.DotSettings +++ b/ImageSharp.sln.DotSettings @@ -345,6 +345,7 @@ FDCT IDCT JPEG + MCU PNG RGB RLE diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/GrayImage.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/GrayImage.cs deleted file mode 100644 index caa30e62d..000000000 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/GrayImage.cs +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright (c) James Jackson-South and contributors. -// Licensed under the Apache License, Version 2.0. -// - -namespace ImageSharp.Formats.Jpg -{ - /// - /// Represents a grayscale image - /// - internal class GrayImage - { - /// - /// Initializes a new instance of the class. - /// - /// The width. - /// The height. - public GrayImage(int width, int height) - { - this.Width = width; - this.Height = height; - this.Pixels = new byte[width * height]; - this.Stride = width; - this.Offset = 0; - } - - /// - /// Prevents a default instance of the class from being created. - /// - private GrayImage() - { - } - - /// - /// Gets or sets the pixels. - /// - public byte[] Pixels { get; set; } - - /// - /// Gets or sets the stride. - /// - public int Stride { get; set; } - - /// - /// Gets or sets the horizontal position. - /// - public int X { get; set; } - - /// - /// Gets or sets the vertical position. - /// - public int Y { get; set; } - - /// - /// Gets or sets the width. - /// - public int Width { get; set; } - - /// - /// Gets or sets the height. - /// - public int Height { get; set; } - - /// - /// Gets or sets the offset - /// - public int Offset { get; set; } - - /// - /// Gets an image made up of a subset of the originals pixels. - /// - /// The x-coordinate of the image. - /// The y-coordinate of the image. - /// The width. - /// The height. - /// - /// The . - /// - public GrayImage Subimage(int x, int y, int width, int height) - { - return new GrayImage - { - Width = width, - Height = height, - Pixels = this.Pixels, - Stride = this.Stride, - Offset = (y * this.Stride) + x - }; - } - - /// - /// Gets the row offset at the given position - /// - /// The y-coordinate of the image. - /// The - public int GetRowOffset(int y) - { - return this.Offset + (y * this.Stride); - } - } -} diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs index a148cc558..e06d644a7 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs @@ -12,6 +12,41 @@ namespace ImageSharp.Formats.Jpg /// internal struct HuffmanTree : IDisposable { + /// + /// 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 + /// + public const int ThRowSize = MaxTh + 1; + + /// + /// Number of Hufman Trees in the Huffman table + /// + public const int NumberOfTrees = (MaxTc + 1) * (MaxTh + 1); + + /// + /// The log-2 size of the Huffman decoder's look-up table. + /// + public const int LutSize = 8; + /// /// Gets or sets the number of codes in the tree. /// @@ -47,25 +82,28 @@ namespace ImageSharp.Formats.Jpg /// public int[] Indices; - private static readonly ArrayPool UshortBuffer = ArrayPool.Create(1 << JpegDecoderCore.LutSize, 50); + private static readonly ArrayPool UshortBuffer = ArrayPool.Create(1 << LutSize, 50); - private static readonly ArrayPool ByteBuffer = ArrayPool.Create(JpegDecoderCore.MaxNCodes, 50); + private static readonly ArrayPool ByteBuffer = ArrayPool.Create(MaxNCodes, 50); - private static readonly ArrayPool IntBuffer = ArrayPool.Create(JpegDecoderCore.MaxCodeLength, 50); + private static readonly ArrayPool IntBuffer = ArrayPool.Create(MaxCodeLength, 50); /// - /// Initializes the Huffman tree + /// Creates and initializes an array of instances of size /// - /// Lut size - /// Max N codes - /// Max code length - public void Init(int lutSize, int maxNCodes, int maxCodeLength) + /// An array of instances representing the Huffman tables + public static HuffmanTree[] CreateHuffmanTrees() { - this.Lut = UshortBuffer.Rent(1 << lutSize); - this.Values = ByteBuffer.Rent(maxNCodes); - this.MinCodes = IntBuffer.Rent(maxCodeLength); - this.MaxCodes = IntBuffer.Rent(maxCodeLength); - this.Indices = IntBuffer.Rent(maxCodeLength); + 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; } /// @@ -79,5 +117,114 @@ namespace ImageSharp.Formats.Jpg IntBuffer.Return(this.MaxCodes, true); IntBuffer.Return(this.Indices, true); } + + /// + /// 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 the Jpeg stream + /// Remaining bits + public 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. + // h.Length is the total number of codes. + this.Length = 0; + + int[] ncodes = new int[MaxCodeLength]; + for (int i = 0; i < ncodes.Length; i++) + { + ncodes[i] = defineHuffmanTablesData[i + 1]; + this.Length += ncodes[i]; + } + + if (this.Length == 0) + { + throw new ImageFormatException("Huffman table has zero length"); + } + + if (this.Length > MaxNCodes) + { + throw new ImageFormatException("Huffman table has excessive length"); + } + + remaining -= this.Length + 17; + if (remaining < 0) + { + throw new ImageFormatException("DHT has wrong length"); + } + + decoder.ReadFull(this.Values, 0, this.Length); + + // Derive the look-up table. + for (int i = 0; i < this.Lut.Length; i++) + { + this.Lut[i] = 0; + } + + uint x = 0, code = 0; + + for (int i = 0; i < LutSize; i++) + { + code <<= 1; + + for (int j = 0; j < ncodes[i]; j++) + { + // The codeLength is 1+i, so shift code by 8-(1+i) to + // calculate the high bits for every 8-bit sequence + // whose codeLength's high bits matches code. + // The high 8 bits of lutValue are the encoded value. + // The low 8 bits are 1 plus the codeLength. + byte base2 = (byte)(code << (7 - i)); + ushort lutValue = (ushort)((this.Values[x] << 8) | (2 + i)); + + for (int k = 0; k < 1 << (7 - i); k++) + { + this.Lut[base2 | k] = lutValue; + } + + code++; + x++; + } + } + + // Derive minCodes, maxCodes, and indices. + int c = 0, index = 0; + for (int i = 0; i < ncodes.Length; i++) + { + int nc = ncodes[i]; + if (nc == 0) + { + this.MinCodes[i] = -1; + this.MaxCodes[i] = -1; + this.Indices[i] = -1; + } + else + { + this.MinCodes[i] = c; + this.MaxCodes[i] = c + nc - 1; + this.Indices[i] = index; + c += nc; + index += nc; + } + + c <<= 1; + } + } + + /// + /// Initializes the Huffman tree + /// + private void Init() + { + this.Lut = UshortBuffer.Rent(1 << LutSize); + this.Values = ByteBuffer.Rent(MaxNCodes); + this.MinCodes = IntBuffer.Rent(MaxCodeLength); + this.MaxCodes = IntBuffer.Rent(MaxCodeLength); + this.Indices = IntBuffer.Rent(MaxCodeLength); + } } } \ 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 new file mode 100644 index 000000000..9fe6fecec --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace ImageSharp.Formats.Jpg +{ + using System.Buffers; + using System.Runtime.CompilerServices; + + /// + /// Represents an area of a Jpeg subimage (channel) + /// + internal struct JpegPixelArea + { + /// + /// Initializes a new instance of the struct from existing data. + /// + /// The pixel array + /// The stride + /// The offset + public JpegPixelArea(byte[] pixels, int striede, int offset) + { + this.Stride = striede; + this.Pixels = pixels; + this.Offset = offset; + } + + /// + /// Gets the pixels. + /// + public byte[] Pixels { get; private set; } + + /// + /// Gets a value indicating whether the instance has been initalized. (Is not default(JpegPixelArea)) + /// + public bool IsInitialized => this.Pixels != null; + + /// + /// Gets or the stride. + /// + public int Stride { get; } + + /// + /// Gets or the offset. + /// + public int Offset { get; } + + /// + /// Gets a of bytes to the pixel area + /// + public MutableSpan Span => new MutableSpan(this.Pixels, this.Offset); + + private static ArrayPool BytePool => ArrayPool.Shared; + + /// + /// 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 = BytePool.Rent(size); + return new JpegPixelArea(pixels, width, 0); + } + + /// + /// Returns to the pool + /// + public void ReturnPooled() + { + if (this.Pixels == null) + { + return; + } + + BytePool.Return(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 + /// The subarea offseted by block indices + public JpegPixelArea GetOffsetedSubAreaForBlock(int bx, int by) + { + int offset = this.Offset + (8 * ((by * this.Stride) + bx)); + return new JpegPixelArea(this.Pixels, this.Stride, offset); + } + + /// + /// Gets the row offset at the given position + /// + /// The y-coordinate of the image. + /// The + public int GetRowOffset(int y) + { + return this.Offset + (y * this.Stride); + } + + /// + /// 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) + { + // Level shift by +128, clip to [0, 255], and write to dst. + 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/JpegScanDecoder.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.cs new file mode 100644 index 000000000..39ee6687b --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.cs @@ -0,0 +1,755 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming +namespace ImageSharp.Formats.Jpg +{ + using System; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + /// + /// Encapsulates the impementation of Jpeg SOS decoder. + /// See JpegScanDecoder.md! + /// + internal unsafe struct JpegScanDecoder + { + /// + /// Number of MCU-s (Minimum Coded Units) in the image along the X axis + /// + public int XNumberOfMCUs; + + /// + /// Number of MCU-s (Minimum Coded Units) in the image along the Y axis + /// + public int YNumberOfMCUs; + + /// + /// The AC table index + /// + private const int AcTableIndex = 1; + + /// + /// The DC table index + /// + private const int DcTableIndex = 0; + + /// + /// X coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0)) + /// + private int bx; + + /// + /// Y coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0)) + /// + private int by; + + // zigStart and zigEnd are the spectral selection bounds. + // ah and al are the successive approximation high and low values. + // The spec calls these values Ss, Se, Ah and Al. + // For progressive JPEGs, these are the two more-or-less independent + // aspects of progression. Spectral selection progression is when not + // all of a block's 64 DCT coefficients are transmitted in one pass. + // For example, three passes could transmit coefficient 0 (the DC + // component), coefficients 1-5, and coefficients 6-63, in zig-zag + // order. Successive approximation is when not all of the bits of a + // band of coefficients are transmitted in one pass. For example, + // three passes could transmit the 6 most significant bits, followed + // by the second-least significant bit, followed by the least + // significant bit. + // For baseline JPEGs, these parameters are hard-coded to 0/63/0/0. + + /// + /// Start index of the zig-zag selection bound + /// + private int zigStart; + + /// + /// End index of the zig-zag selection bound + /// + private int zigEnd; + + /// + /// Successive approximation high value + /// + private int ah; + + /// + /// Successive approximation high and low value + /// + private int al; + + /// + /// The number of component scans + /// + private int componentScanCount; + + /// + /// End-of-Band run, specified in section G.1.2.2. + /// + private ushort eobRun; + + /// + /// The buffer + /// + private ComputationData data; + + /// + /// Pointers to elements of + /// + private DataPointers pointers; + + /// + /// Initializes the default instance after creation. + /// + /// Pointer to on the stack + /// The instance + /// The remaining bytes in the segment block. + public static void Init(JpegScanDecoder* p, JpegDecoderCore decoder, int remaining) + { + p->data = ComputationData.Create(); + p->pointers = new DataPointers(&p->data); + p->InitImpl(decoder, remaining); + } + + /// + /// Reads the blocks from the -s stream, and processes them into the corresponding instances. + /// + /// The instance + public void ProcessBlocks(JpegDecoderCore decoder) + { + int blockCount = 0; + int mcu = 0; + byte expectedRst = JpegConstants.Markers.RST0; + + for (int my = 0; my < this.YNumberOfMCUs; my++) + { + for (int mx = 0; mx < this.XNumberOfMCUs; mx++) + { + for (int i = 0; i < this.componentScanCount; i++) + { + int compIndex = this.pointers.Scan[i].Index; + int hi = decoder.ComponentArray[compIndex].HorizontalFactor; + int vi = decoder.ComponentArray[compIndex].VerticalFactor; + + for (int j = 0; j < hi * vi; j++) + { + // The blocks are traversed one MCU at a time. For 4:2:0 chroma + // subsampling, there are four Y 8x8 blocks in every 16x16 MCU. + // For a baseline 32x16 pixel image, the Y blocks visiting order is: + // 0 1 4 5 + // 2 3 6 7 + // For progressive images, the interleaved scans (those with component count > 1) + // are traversed as above, but non-interleaved scans are traversed left + // to right, top to bottom: + // 0 1 2 3 + // 4 5 6 7 + // Only DC scans (zigStart == 0) can be interleave AC scans must have + // only one component. + // To further complicate matters, for non-interleaved scans, there is no + // data for any blocks that are inside the image at the MCU level but + // outside the image at the pixel level. For example, a 24x16 pixel 4:2:0 + // progressive image consists of two 16x16 MCUs. The interleaved scans + // will process 8 Y blocks: + // 0 1 4 5 + // 2 3 6 7 + // The non-interleaved scans will process only 6 Y blocks: + // 0 1 2 + // 3 4 5 + if (this.componentScanCount != 1) + { + this.bx = (hi * mx) + (j % hi); + this.by = (vi * my) + (j / hi); + } + else + { + int q = this.XNumberOfMCUs * hi; + this.bx = blockCount % q; + this.by = blockCount / q; + blockCount++; + if (this.bx * 8 >= decoder.ImageWidth || this.by * 8 >= decoder.ImageHeight) + { + continue; + } + } + + int qtIndex = decoder.ComponentArray[compIndex].Selector; + + // TODO: Reading & processing blocks should be done in 2 separate loops. The second one could be parallelized. The first one could be async. + this.data.QuantiazationTable = decoder.QuantizationTables[qtIndex]; + + // Load the previous partially decoded coefficients, if applicable. + if (decoder.IsProgressive) + { + int blockIndex = ((this.by * this.XNumberOfMCUs) * hi) + this.bx; + this.data.Block = decoder.ProgCoeffs[compIndex][blockIndex]; + } + else + { + this.data.Block.Clear(); + } + + this.ProcessBlockImpl(decoder, i, compIndex, hi); + } + + // for j + } + + // for i + mcu++; + + if (decoder.RestartInterval > 0 && mcu % decoder.RestartInterval == 0 && mcu < this.XNumberOfMCUs * this.YNumberOfMCUs) + { + // A more sophisticated decoder could use RST[0-7] markers to resynchronize from corrupt input, + // but this one assumes well-formed input, and hence the restart marker follows immediately. + decoder.ReadFull(decoder.Temp, 0, 2); + if (decoder.Temp[0] != 0xff || decoder.Temp[1] != expectedRst) + { + throw new ImageFormatException("Bad RST marker"); + } + + expectedRst++; + if (expectedRst == JpegConstants.Markers.RST7 + 1) + { + expectedRst = JpegConstants.Markers.RST0; + } + + // Reset the Huffman decoder. + decoder.Bits = default(Bits); + + // Reset the DC components, as per section F.2.1.3.1. + this.ResetDc(); + + // Reset the progressive decoder state, as per section G.1.2.2. + this.eobRun = 0; + } + } + + // for mx + } + } + + private void ResetDc() + { + Unsafe.InitBlock(this.pointers.Dc, default(byte), sizeof(int) * JpegDecoderCore.MaxComponents); + } + + /// + /// The implementation part of as an instance method. + /// + /// The + /// The remaining bytes + private void InitImpl(JpegDecoderCore decoder, int remaining) + { + if (decoder.ComponentCount == 0) + { + throw new ImageFormatException("Missing SOF marker"); + } + + if (remaining < 6 || 4 + (2 * decoder.ComponentCount) < remaining || remaining % 2 != 0) + { + throw new ImageFormatException("SOS has wrong length"); + } + + decoder.ReadFull(decoder.Temp, 0, remaining); + this.componentScanCount = decoder.Temp[0]; + + int scanComponentCountX2 = 2 * this.componentScanCount; + if (remaining != 4 + scanComponentCountX2) + { + throw new ImageFormatException("SOS length inconsistent with number of components"); + } + + int totalHv = 0; + + for (int i = 0; i < this.componentScanCount; i++) + { + this.ProcessScanImpl(decoder, i, ref this.pointers.Scan[i], ref totalHv); + } + + // Section B.2.3 states that if there is more than one component then the + // total H*V values in a scan must be <= 10. + if (decoder.ComponentCount > 1 && totalHv > 10) + { + throw new ImageFormatException("Total sampling factors too large."); + } + + this.zigEnd = Block8x8F.ScalarCount - 1; + + if (decoder.IsProgressive) + { + this.zigStart = decoder.Temp[1 + scanComponentCountX2]; + this.zigEnd = decoder.Temp[2 + scanComponentCountX2]; + this.ah = decoder.Temp[3 + scanComponentCountX2] >> 4; + this.al = decoder.Temp[3 + scanComponentCountX2] & 0x0f; + + if ((this.zigStart == 0 && this.zigEnd != 0) || this.zigStart > this.zigEnd + || this.zigEnd >= Block8x8F.ScalarCount) + { + throw new ImageFormatException("Bad spectral selection bounds"); + } + + if (this.zigStart != 0 && this.componentScanCount != 1) + { + throw new ImageFormatException("Progressive AC coefficients for more than one component"); + } + + if (this.ah != 0 && this.ah != this.al + 1) + { + throw new ImageFormatException("Bad successive approximation values"); + } + } + + // XNumberOfMCUs and YNumberOfMCUs are the number of MCUs (Minimum Coded Units) in the image. + int h0 = decoder.ComponentArray[0].HorizontalFactor; + int v0 = decoder.ComponentArray[0].VerticalFactor; + this.XNumberOfMCUs = (decoder.ImageWidth + (8 * h0) - 1) / (8 * h0); + this.YNumberOfMCUs = (decoder.ImageHeight + (8 * v0) - 1) / (8 * v0); + + if (decoder.IsProgressive) + { + for (int i = 0; i < this.componentScanCount; i++) + { + int compIndex = this.pointers.Scan[i].Index; + if (decoder.ProgCoeffs[compIndex] == null) + { + int size = this.XNumberOfMCUs * this.YNumberOfMCUs * decoder.ComponentArray[compIndex].HorizontalFactor + * decoder.ComponentArray[compIndex].VerticalFactor; + + decoder.ProgCoeffs[compIndex] = new Block8x8F[size]; + } + } + } + } + + /// + /// Process the current block at (, ) + /// + /// The decoder + /// The index of the scan + /// The component index + /// Horizontal sampling factor at the given component index + private void ProcessBlockImpl(JpegDecoderCore decoder, int i, int compIndex, int hi) + { + var b = this.pointers.Block; + + int huffmannIdx = (AcTableIndex * HuffmanTree.ThRowSize) + this.pointers.Scan[i].AcTableSelector; + if (this.ah != 0) + { + this.Refine(decoder, ref decoder.HuffmanTrees[huffmannIdx], 1 << this.al); + } + else + { + int zig = this.zigStart; + if (zig == 0) + { + zig++; + + // Decode the DC coefficient, as specified in section F.2.2.1. + byte value = + decoder.DecodeHuffman( + ref decoder.HuffmanTrees[(DcTableIndex * HuffmanTree.ThRowSize) + this.pointers.Scan[i].DcTableSelector]); + if (value > 16) + { + throw new ImageFormatException("Excessive DC component"); + } + + int deltaDC = decoder.Bits.ReceiveExtend(value, decoder); + this.pointers.Dc[compIndex] += deltaDC; + + // b[0] = dc[compIndex] << al; + Block8x8F.SetScalarAt(b, 0, this.pointers.Dc[compIndex] << this.al); + } + + if (zig <= this.zigEnd && this.eobRun > 0) + { + this.eobRun--; + } + else + { + // Decode the AC coefficients, as specified in section F.2.2.2. + for (; zig <= this.zigEnd; zig++) + { + byte value = decoder.DecodeHuffman(ref decoder.HuffmanTrees[huffmannIdx]); + byte val0 = (byte)(value >> 4); + byte val1 = (byte)(value & 0x0f); + if (val1 != 0) + { + zig += val0; + if (zig > this.zigEnd) + { + break; + } + + int ac = decoder.Bits.ReceiveExtend(val1, decoder); + + // b[Unzig[zig]] = ac << al; + Block8x8F.SetScalarAt(b, this.pointers.Unzig[zig], ac << this.al); + } + else + { + if (val0 != 0x0f) + { + this.eobRun = (ushort)(1 << val0); + if (val0 != 0) + { + this.eobRun |= (ushort)decoder.DecodeBits(val0); + } + + this.eobRun--; + break; + } + + zig += 0x0f; + } + } + } + } + + if (decoder.IsProgressive) + { + if (this.zigEnd != Block8x8F.ScalarCount - 1 || this.al != 0) + { + // We haven't completely decoded this 8x8 block. Save the coefficients. + // this.ProgCoeffs[compIndex][((@by * XNumberOfMCUs) * hi) + bx] = b.Clone(); + decoder.ProgCoeffs[compIndex][((this.by * this.XNumberOfMCUs) * hi) + this.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, this.pointers.QuantiazationTable, this.pointers.Unzig); + + DCT.TransformIDCT(ref *b, ref *this.pointers.Temp1, ref *this.pointers.Temp2); + + var destChannel = decoder.GetDestinationChannel(compIndex); + var destArea = destChannel.GetOffsetedSubAreaForBlock(this.bx, this.by); + destArea.LoadColorsFrom(this.pointers.Temp1, this.pointers.Temp2); + } + + private void ProcessScanImpl(JpegDecoderCore decoder, int i, ref Scan currentScan, ref int totalHv) + { + // Component selector. + int cs = decoder.Temp[1 + (2 * i)]; + int compIndex = -1; + for (int j = 0; j < decoder.ComponentCount; j++) + { + // Component compv = ; + if (cs == decoder.ComponentArray[j].Identifier) + { + compIndex = j; + } + } + + if (compIndex < 0) + { + throw new ImageFormatException("Unknown component selector"); + } + + currentScan.Index = (byte)compIndex; + + this.ProcessComponentImpl(decoder, i, ref currentScan, ref totalHv, ref decoder.ComponentArray[compIndex]); + } + + private void ProcessComponentImpl( + JpegDecoderCore decoder, + int i, + ref Scan currentScan, + 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 == this.pointers.Scan[j].Index) + { + throw new ImageFormatException("Repeated component selector"); + } + } + + totalHv += currentComponent.HorizontalFactor * currentComponent.VerticalFactor; + + currentScan.DcTableSelector = (byte)(decoder.Temp[2 + (2 * i)] >> 4); + if (currentScan.DcTableSelector > HuffmanTree.MaxTh) + { + throw new ImageFormatException("Bad DC table selector value"); + } + + currentScan.AcTableSelector = (byte)(decoder.Temp[2 + (2 * i)] & 0x0f); + if (currentScan.AcTableSelector > HuffmanTree.MaxTh) + { + throw new ImageFormatException("Bad AC table selector value"); + } + } + + /// + /// Decodes a successive approximation refinement block, as specified in section G.1.2. + /// + /// The decoder instance + /// The Huffman tree + /// The low transform offset + private void Refine(JpegDecoderCore decoder, ref HuffmanTree h, int delta) + { + Block8x8F* b = this.pointers.Block; + + // Refining a DC component is trivial. + if (this.zigStart == 0) + { + if (this.zigEnd != 0) + { + throw new ImageFormatException("Invalid state for zig DC component"); + } + + bool bit = decoder.DecodeBit(); + if (bit) + { + int stuff = (int)Block8x8F.GetScalarAt(b, 0); + + // int stuff = (int)b[0]; + stuff |= delta; + + // b[0] = stuff; + Block8x8F.SetScalarAt(b, 0, stuff); + } + + return; + } + + // Refining AC components is more complicated; see sections G.1.2.2 and G.1.2.3. + int zig = this.zigStart; + if (this.eobRun == 0) + { + for (; zig <= this.zigEnd; zig++) + { + bool done = false; + int z = 0; + byte val = decoder.DecodeHuffman(ref h); + int val0 = val >> 4; + int val1 = val & 0x0f; + + switch (val1) + { + case 0: + if (val0 != 0x0f) + { + this.eobRun = (ushort)(1 << val0); + if (val0 != 0) + { + this.eobRun |= (ushort)decoder.DecodeBits(val0); + } + + done = true; + } + + break; + case 1: + z = delta; + bool bit = decoder.DecodeBit(); + if (!bit) + { + z = -z; + } + + break; + default: + throw new ImageFormatException("Unexpected Huffman code"); + } + + if (done) + { + break; + } + + zig = this.RefineNonZeroes(decoder, zig, val0, delta); + if (zig > this.zigEnd) + { + throw new ImageFormatException($"Too many coefficients {zig} > {this.zigEnd}"); + } + + if (z != 0) + { + // b[Unzig[zig]] = z; + Block8x8F.SetScalarAt(b, this.pointers.Unzig[zig], z); + } + } + } + + if (this.eobRun > 0) + { + this.eobRun--; + this.RefineNonZeroes(decoder, zig, -1, delta); + } + } + + /// + /// Refines non-zero entries of b in zig-zag order. + /// If >= 0, the first zero entries are skipped over. + /// + /// The decoder + /// The zig-zag start index + /// The non-zero entry + /// The low transform offset + /// The + private int RefineNonZeroes(JpegDecoderCore decoder, int zig, int nz, int delta) + { + var b = this.pointers.Block; + for (; zig <= this.zigEnd; zig++) + { + int u = this.pointers.Unzig[zig]; + float bu = Block8x8F.GetScalarAt(b, u); + + // TODO: Are the equality comparsions OK with floating point values? Isn't an epsilon value necessary? + if (bu == 0) + { + if (nz == 0) + { + break; + } + + nz--; + continue; + } + + bool bit = decoder.DecodeBit(); + if (!bit) + { + continue; + } + + if (bu >= 0) + { + // b[u] += delta; + Block8x8F.SetScalarAt(b, u, bu + delta); + } + else + { + // b[u] -= delta; + Block8x8F.SetScalarAt(b, u, bu - delta); + } + } + + return zig; + } + + /// + /// Holds the "large" data blocks needed for computations + /// + [StructLayout(LayoutKind.Sequential)] + public struct ComputationData + { + /// + /// The main input block + /// + public Block8x8F Block; + + /// + /// Temporal block 1 to store intermediate and/or final computation results + /// + public Block8x8F Temp1; + + /// + /// Temporal block 2 to store intermediate and/or final computation results + /// + public Block8x8F Temp2; + + /// + /// The quantization table as + /// + public Block8x8F QuantiazationTable; + + /// + /// The jpeg unzig data + /// + public UnzigData Unzig; + + /// + /// The no-idea-what's this data + /// + public fixed byte ScanData[3 * JpegDecoderCore.MaxComponents]; + + /// + /// The DC component values + /// + public fixed int Dc[JpegDecoderCore.MaxComponents]; + + /// + /// Creates and initializes a new instance + /// + /// The + public static ComputationData Create() + { + ComputationData data = default(ComputationData); + data.Unzig = UnzigData.Create(); + return data; + } + } + + /// + /// Contains pointers to the memory regions of so they can be easily passed around to pointer based utility methods of + /// + public struct DataPointers + { + /// + /// Pointer to + /// + public Block8x8F* Block; + + /// + /// Pointer to + /// + public Block8x8F* Temp1; + + /// + /// Pointer to + /// + public Block8x8F* Temp2; + + /// + /// Pointer to + /// + public Block8x8F* QuantiazationTable; + + /// + /// Pointer to as int* + /// + public int* Unzig; + + /// + /// Pointer to as Scan* + /// + public Scan* Scan; + + /// + /// Pointer to + /// + public int* Dc; + + /// + /// Initializes a new instance of the struct. + /// + /// The pointer pointing to + public DataPointers(ComputationData* basePtr) + { + this.Block = &basePtr->Block; + this.Temp1 = &basePtr->Temp1; + this.Temp2 = &basePtr->Temp2; + this.QuantiazationTable = &basePtr->QuantiazationTable; + this.Unzig = basePtr->Unzig.Data; + this.Scan = (Scan*)basePtr->ScanData; + this.Dc = basePtr->Dc; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.md b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.md new file mode 100644 index 000000000..215f21807 --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.md @@ -0,0 +1,25 @@ +## JpegScanDecoder +Encapsulates the impementation of the Jpeg top-to bottom scan decoder triggered by the `SOS` marker. +The implementation is optimized to hold most of the necessary data in a single value type, which is intended to be used as an on-stack object. + +#### Benefits: +- Maximized locality of reference by keeping most of the operation data on the stack +- Reaching this without long parameter lists, most of the values describing the state of the decoder algorithm +are members of the `JpegScanDecoder` struct +- Most of the logic related to Scan decoding is refactored & simplified now to live in the methods of `JpegScanDecoder` +- The first step is done towards separating the stream reading from block processing. They can be refactored later to be executed in two disctinct loops. + - The input processing loop can be `async` + - The block processing loop can be parallelized + +#### Data layout + +|JpegScanDecoder | +|-------------------| +|Variables | +|ComputationData | +|DataPointers | + +- **ComputationData** holds the "large" data blocks needed for computations (Mostly `Block8x8F`-s) +- **DataPointers** contains pointers to the memory regions of `ComponentData` so they can be easily passed around to pointer based utility methods of `Block8x8F` + + diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/Scan.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/Scan.cs new file mode 100644 index 000000000..799c3cc31 --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/Scan.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpg +{ + using System.Runtime.InteropServices; + + /// + /// Represents a component scan + /// + [StructLayout(LayoutKind.Sequential)] + internal struct Scan + { + /// + /// Gets or sets the component index. + /// + public byte Index; + + /// + /// Gets or sets the DC table selector + /// + public byte DcTableSelector; + + /// + /// Gets or sets the AC table selector + /// + public byte AcTableSelector; + } +} \ 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 cba9c4461..a5ca9796b 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs @@ -2,41 +2,51 @@ // 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) /// - internal class YCbCrImage + internal class YCbCrImage : IDisposable { + // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P +#pragma warning disable SA1401 // FieldsMustBePrivate + /// + /// Gets the luminance components channel as . + /// + public JpegPixelArea YChannel; + + /// + /// Gets the blue chroma components channel as . + /// + public JpegPixelArea CbChannel; + + /// + /// Gets an offseted to the Cr channel + /// + public JpegPixelArea CrChannel; +#pragma warning restore SA1401 + /// - /// 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 cw, ch; - YCbCrSize(width, height, ratio, out cw, out ch); - this.YChannel = new byte[width * height]; - this.CbChannel = new byte[cw * ch]; - this.CrChannel = new byte[cw * ch]; + Size cSize = CalculateChrominanceSize(width, height, ratio); + this.Ratio = ratio; this.YStride = width; - this.CStride = cw; - this.X = 0; - this.Y = 0; - this.Width = width; - this.Height = height; - } + this.CStride = cSize.Width; - /// - /// Prevents a default instance of the class from being created. - /// - private YCbCrImage() - { + this.YChannel = JpegPixelArea.CreatePooled(width, height); + this.CbChannel = JpegPixelArea.CreatePooled(cSize.Width, cSize.Height); + this.CrChannel = JpegPixelArea.CreatePooled(cSize.Width, cSize.Height); } /// @@ -76,60 +86,15 @@ namespace ImageSharp.Formats.Jpg } /// - /// Gets or sets the luminance components channel. - /// - public byte[] YChannel { get; set; } - - /// - /// Gets or sets the blue chroma components channel. + /// Gets the Y slice index delta between vertically adjacent pixels. /// - public byte[] CbChannel { get; set; } + public int YStride { get; } /// - /// Gets or sets the red chroma components channel. - /// - public byte[] CrChannel { get; set; } - - /// - /// Gets or sets the Y slice index delta between vertically adjacent pixels. - /// - public int YStride { get; set; } - - /// - /// Gets or sets the red and blue chroma slice index delta between vertically adjacent pixels + /// Gets the red and blue chroma slice index delta between vertically adjacent pixels /// that map to separate chroma samples. /// - public int CStride { get; set; } - - /// - /// Gets or sets the index of the first luminance element. - /// - public int YOffset { get; set; } - - /// - /// Gets or sets the index of the first element of red or blue chroma. - /// - public int COffset { get; set; } - - /// - /// Gets or sets the horizontal position. - /// - public int X { get; set; } - - /// - /// Gets or sets the vertical position. - /// - public int Y { get; set; } - - /// - /// Gets or sets the width. - /// - public int Width { get; set; } - - /// - /// Gets or sets the height. - /// - public int Height { get; set; } + public int CStride { get; } /// /// Gets or sets the subsampling ratio. @@ -137,43 +102,13 @@ namespace ImageSharp.Formats.Jpg public YCbCrSubsampleRatio Ratio { get; set; } /// - /// Gets an image made up of a subset of the originals pixels. + /// Disposes the returning rented arrays to the pools. /// - /// The x-coordinate of the image. - /// The y-coordinate of the image. - /// The width. - /// The height. - /// - /// The . - /// - public YCbCrImage Subimage(int x, int y, int width, int height) + public void Dispose() { - YCbCrImage ret = new YCbCrImage - { - Width = width, - Height = height, - YChannel = this.YChannel, - CbChannel = this.CbChannel, - CrChannel = this.CrChannel, - Ratio = this.Ratio, - YStride = this.YStride, - CStride = this.CStride, - YOffset = (y * this.YStride) + x, - COffset = (y * this.CStride) + x - }; - return ret; - } - - /// - /// 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; + this.YChannel.ReturnPooled(); + this.CbChannel.ReturnPooled(); + this.CrChannel.ReturnPooled(); } /// @@ -181,7 +116,7 @@ namespace ImageSharp.Formats.Jpg /// /// The row number. /// - /// The . + /// The . /// public int GetRowCOffset(int y) { @@ -202,45 +137,46 @@ namespace ImageSharp.Formats.Jpg } } + /// + /// 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) + /// The of the chrominance channel + internal static Size CalculateChrominanceSize( + int width, + int height, + YCbCrSubsampleRatio ratio) { switch (ratio) { case YCbCrSubsampleRatio.YCbCrSubsampleRatio422: - chromaWidth = (width + 1) / 2; - chromaHeight = height; - break; + return new Size((width + 1) / 2, height); case YCbCrSubsampleRatio.YCbCrSubsampleRatio420: - chromaWidth = (width + 1) / 2; - chromaHeight = (height + 1) / 2; - break; + return new Size((width + 1) / 2, (height + 1) / 2); case YCbCrSubsampleRatio.YCbCrSubsampleRatio440: - chromaWidth = width; - chromaHeight = (height + 1) / 2; - break; + return new Size(width, (height + 1) / 2); case YCbCrSubsampleRatio.YCbCrSubsampleRatio411: - chromaWidth = (width + 3) / 4; - chromaHeight = height; - break; + return new Size((width + 3) / 4, height); case YCbCrSubsampleRatio.YCbCrSubsampleRatio410: - chromaWidth = (width + 3) / 4; - chromaHeight = (height + 1) / 2; - break; + return new Size((width + 3) / 4, (height + 1) / 2); default: - // Default to 4:4:4 subsampling. - chromaWidth = width; - chromaHeight = height; - break; + return new Size(width, height); } } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index 761ad891e..284ae807b 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -2,14 +2,13 @@ // 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; /// @@ -17,37 +16,23 @@ namespace ImageSharp.Formats /// internal unsafe class JpegDecoderCore : IDisposable { - /// - /// The maximum (inclusive) number of bits in a Huffman code. - /// - internal const int MaxCodeLength = 16; - - /// - /// The maximum (inclusive) number of codes in a Huffman tree. - /// - internal const int MaxNCodes = 256; - - /// - /// The log-2 size of the Huffman decoder's look-up table. - /// - internal const int LutSize = 8; - /// /// The maximum number of color components /// - private const int MaxComponents = 4; + public const int MaxComponents = 4; + // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P +#pragma warning disable SA1401 // FieldsMustBePrivate /// - /// The maximum number of Huffman table classes + /// Holds the unprocessed bits that have been taken from the byte-stream. /// - private const int MaxTc = 1; + public Bits Bits; /// - /// The maximum number of Huffman table identifiers + /// The byte buffer. /// - private const int MaxTh = 3; - - private const int ThRowSize = MaxTh + 1; + public Bytes Bytes; +#pragma warning restore SA401 /// /// The maximum number of quantization tables @@ -55,74 +40,39 @@ namespace ImageSharp.Formats private const int MaxTq = 3; /// - /// The DC table index - /// - private const int DcTable = 0; - - /// - /// The AC table index - /// - private const int AcTable = 1; - - /// - /// The component array - /// - private readonly Component[] componentArray; - - /// - /// Saved state between progressive-mode scans. - /// - private readonly Block8x8F[][] progCoeffs; - - /// - /// The huffman trees - /// - private readonly HuffmanTree[] huffmanTrees; - - /// - /// Quantization tables, in zigzag order. - /// - private readonly Block8x8F[] quantizationTables; - - /// - /// A temporary buffer for holding pixels - /// - private readonly byte[] temp; - - /// - /// 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. + /// The black image to decode to. /// - private Bits bits; + private JpegPixelArea blackImage; /// - /// The image width + /// A grayscale image to decode to. /// - private int imageWidth; + private JpegPixelArea grayImage; /// - /// The image height + /// The horizontal resolution. Calculated if the image has a JFIF header. /// - private int imageHeight; + private short horizontalResolution; /// - /// The number of color components within the image. + /// Whether the image has a JFIF header /// - private int componentCount; + private bool isJfif; /// - /// A grayscale image to decode to. + /// The vertical resolution. Calculated if the image has a JFIF header. /// - private GrayImage grayImage; + private short verticalResolution; /// /// The full color image to decode to. @@ -130,125 +80,91 @@ namespace ImageSharp.Formats private YCbCrImage ycbcrImage; /// - /// The array of keyline pixels in a CMYK image + /// Initializes a new instance of the class. /// - private byte[] blackPixels; + public JpegDecoderCore() + { + this.HuffmanTrees = HuffmanTree.CreateHuffmanTrees(); + this.QuantizationTables = new Block8x8F[MaxTq + 1]; + this.Temp = new byte[2 * Block8x8F.ScalarCount]; + this.ComponentArray = new Component[MaxComponents]; + this.ProgCoeffs = new Block8x8F[MaxComponents][]; + this.Bits = default(Bits); + this.Bytes = Bytes.Create(); + } /// - /// The width in bytes or a single row of keyline pixels in a CMYK image + /// ReadByteStuffedByte was throwing exceptions on normal execution path (very inefficent) + /// It's better tho have an error code for this! /// - private int blackStride; + internal enum ErrorCodes + { + /// + /// NoError + /// + NoError, - /// - /// The restart interval - /// - private int restartInterval; + /// + /// MissingFF00 + /// + MissingFF00 + } /// - /// Whether the image is interlaced (progressive) + /// Gets the component array /// - private bool isProgressive; + public Component[] ComponentArray { get; } /// - /// Whether the image has a JFIF header + /// Gets the huffman trees /// - private bool isJfif; + public HuffmanTree[] HuffmanTrees { get; } /// - /// Whether the image is in CMYK format with an App14 marker + /// Gets the saved state between progressive-mode scans. /// - private bool adobeTransformValid; + public Block8x8F[][] ProgCoeffs { get; } /// - /// The App14 marker color-space + /// Gets the quantization tables, in zigzag order. /// - private byte adobeTransform; + public Block8x8F[] QuantizationTables { get; } /// - /// End-of-Band run, specified in section G.1.2.2. + /// Gets the temporary buffer for holding pixel (and other?) data /// - private ushort eobRun; + // TODO: the usage rules of this buffer seem to be unclean + need to consider stack-allocating it for perf + public byte[] Temp { get; } /// - /// The horizontal resolution. Calculated if the image has a JFIF header. + /// Gets the number of color components within the image. /// - private short horizontalResolution; + public int ComponentCount { get; private set; } /// - /// The vertical resolution. Calculated if the image has a JFIF header. + /// Gets the image height /// - private short verticalResolution; - - private int blockIndex; + public int ImageHeight { get; private set; } /// - /// Initializes a new instance of the class. + /// Gets the image width /// - public JpegDecoderCore() - { - // this.huffmanTrees = new Huffman[MaxTc + 1, MaxTh + 1]; - this.huffmanTrees = new HuffmanTree[(MaxTc + 1) * (MaxTh + 1)]; - - this.quantizationTables = new Block8x8F[MaxTq + 1]; - this.temp = new byte[2 * Block8x8F.ScalarCount]; - this.componentArray = new Component[MaxComponents]; - this.progCoeffs = new Block8x8F[MaxComponents][]; - this.bits = default(Bits); - this.bytes = Bytes.Create(); - - // TODO: This looks like it could be static. - for (int i = 0; i < MaxTc + 1; i++) - { - for (int j = 0; j < MaxTh + 1; j++) - { - this.huffmanTrees[(i * ThRowSize) + j].Init(LutSize, MaxNCodes, MaxCodeLength); - } - } - } + public int ImageWidth { get; private set; } /// - /// ReadByteStuffedByte was throwing exceptions on normal execution path (very inefficent) - /// It's better tho have an error code for this! + /// Gets the input stream. /// - internal enum ErrorCodes - { - /// - /// NoError - /// - NoError, - - /// - /// MissingFF00 - /// - MissingFF00 - } + public Stream InputStream { get; private set; } /// - /// Gets or sets the byte buffer. + /// Gets a value indicating whether the image is interlaced (progressive) /// - public Bytes Bytes - { - get - { - return this.bytes; - } - - set - { - this.bytes = value; - } - } + public bool IsProgressive { get; private set; } /// - /// Gets the input stream. + /// Gets the restart interval /// - public Stream InputStream - { - get - { - return this.inputStream; - } - } + public int RestartInterval { get; private set; } /// /// Decodes the image from the specified this._stream and sets @@ -261,11 +177,11 @@ namespace ImageSharp.Formats public void Decode(Image image, Stream stream, bool configOnly) where TColor : struct, IPackedPixel, IEquatable { - this.inputStream = stream; + this.InputStream = stream; // Check for the Start Of Image marker. - this.ReadFull(this.temp, 0, 2); - if (this.temp[0] != JpegConstants.Markers.XFF || this.temp[1] != JpegConstants.Markers.SOI) + this.ReadFull(this.Temp, 0, 2); + if (this.Temp[0] != JpegConstants.Markers.XFF || this.Temp[1] != JpegConstants.Markers.SOI) { throw new ImageFormatException("Missing SOI marker."); } @@ -273,8 +189,8 @@ namespace ImageSharp.Formats // Process the remaining segments until the End Of Image marker. while (true) { - this.ReadFull(this.temp, 0, 2); - while (this.temp[0] != 0xff) + this.ReadFull(this.Temp, 0, 2); + while (this.Temp[0] != 0xff) { // Strictly speaking, this is a format error. However, libjpeg is // liberal in what it accepts. As of version 9, next_marker in @@ -293,11 +209,11 @@ namespace ImageSharp.Formats // mechanism within a scan (the RST[0-7] markers). // Note that extraneous 0xff bytes in e.g. SOS data are escaped as // "\xff\x00", and so are detected a little further down below. - this.temp[0] = this.temp[1]; - this.temp[1] = this.ReadByte(); + this.Temp[0] = this.Temp[1]; + this.Temp[1] = this.ReadByte(); } - byte marker = this.temp[1]; + byte marker = this.Temp[1]; if (marker == 0) { // Treat "\xff\x00" as extraneous data. @@ -330,8 +246,8 @@ namespace ImageSharp.Formats // Read the 16-bit length of the segment. The value includes the 2 bytes for the // length itself, so we subtract 2 to get the number of remaining bytes. - this.ReadFull(this.temp, 0, 2); - int remaining = (this.temp[0] << 8) + this.temp[1] - 2; + this.ReadFull(this.Temp, 0, 2); + int remaining = (this.Temp[0] << 8) + this.Temp[1] - 2; if (remaining < 0) { throw new ImageFormatException("Short segment length."); @@ -342,7 +258,7 @@ namespace ImageSharp.Formats case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - this.isProgressive = marker == JpegConstants.Markers.SOF2; + this.IsProgressive = marker == JpegConstants.Markers.SOF2; this.ProcessStartOfFrameMarker(remaining); if (configOnly && this.isJfif) { @@ -420,17 +336,18 @@ namespace ImageSharp.Formats } } - if (this.grayImage != null) + if (this.grayImage.IsInitialized) { - this.ConvertFromGrayScale(this.imageWidth, this.imageHeight, image); + this.ConvertFromGrayScale(this.ImageWidth, this.ImageHeight, image); } else if (this.ycbcrImage != null) { - if (this.componentCount == 4) + if (this.ComponentCount == 4) { 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 @@ -438,26 +355,26 @@ namespace ImageSharp.Formats // TODO: YCbCrA? if (this.adobeTransform == JpegConstants.Adobe.ColorTransformYcck) { - this.ConvertFromYcck(this.imageWidth, this.imageHeight, image); + this.ConvertFromYcck(this.ImageWidth, this.ImageHeight, image); } else if (this.adobeTransform == JpegConstants.Adobe.ColorTransformUnknown) { // Assume CMYK - this.ConvertFromCmyk(this.imageWidth, this.imageHeight, image); + this.ConvertFromCmyk(this.ImageWidth, this.ImageHeight, image); } return; } - if (this.componentCount == 3) + if (this.ComponentCount == 3) { if (this.IsRGB()) { - this.ConvertFromRGB(this.imageWidth, this.imageHeight, image); + this.ConvertFromRGB(this.ImageWidth, this.ImageHeight, image); return; } - this.ConvertFromYCbCr(this.imageWidth, this.imageHeight, image); + this.ConvertFromYCbCr(this.ImageWidth, this.ImageHeight, image); return; } @@ -474,173 +391,116 @@ namespace ImageSharp.Formats /// public void Dispose() { - for (int i = 0; i < this.huffmanTrees.Length; i++) + for (int i = 0; i < this.HuffmanTrees.Length; i++) { - this.huffmanTrees[i].Dispose(); + this.HuffmanTrees[i].Dispose(); } - this.bytes.Dispose(); + this.ycbcrImage?.Dispose(); + this.Bytes.Dispose(); + this.grayImage.ReturnPooled(); + this.blackImage.ReturnPooled(); } /// /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. /// - /// The + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal byte ReadByte() + public byte ReadByte() { - return this.bytes.ReadByte(this.inputStream); + return this.Bytes.ReadByte(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. + /// Decodes a single bit /// - /// The pixel format. - /// The packed pixel. - /// The y luminance component. - /// The cb chroma component. - /// The cr chroma component. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) - where TColor : struct, IPackedPixel, IEquatable + /// The + public bool DecodeBit() { - int ccb = cb - 128; - int ccr = cr - 128; - - byte r = (byte)(y + (1.402F * ccr)).Clamp(0, 255); - byte g = (byte)(y - (0.34414F * ccb) - (0.71414F * ccr)).Clamp(0, 255); - byte b = (byte)(y + (1.772F * ccb)).Clamp(0, 255); + if (this.Bits.UnreadBits == 0) + { + ErrorCodes errorCode = this.Bits.EnsureNBits(1, this); + if (errorCode != ErrorCodes.NoError) + { + throw new MissingFF00Exception(); + } + } - packed.PackFromBytes(r, g, b, 255); + bool ret = (this.Bits.Accumulator & this.Bits.Mask) != 0; + this.Bits.UnreadBits--; + this.Bits.Mask >>= 1; + return ret; } /// - /// Processes a Define Huffman Table marker, and initializes a huffman - /// struct from its contents. Specified in section B.2.4.2. + /// Reads exactly length bytes into data. It does not care about byte stuffing. /// - /// The remaining bytes in the segment block. - private void ProcessDefineHuffmanTablesMarker(int remaining) + /// The data to write to. + /// The offset in the source buffer + /// The number of bytes to read + public void ReadFull(byte[] data, int offset, int length) { - while (remaining > 0) + // Unread the overshot bytes, if any. + if (this.Bytes.UnreadableBytes != 0) { - if (remaining < 17) + if (this.Bits.UnreadBits >= 8) { - throw new ImageFormatException("DHT has wrong length"); + this.UnreadByteStuffedByte(); } - this.ReadFull(this.temp, 0, 17); + this.Bytes.UnreadableBytes = 0; + } - int tc = this.temp[0] >> 4; - if (tc > MaxTc) + while (length > 0) + { + if (this.Bytes.J - this.Bytes.I >= length) { - throw new ImageFormatException("Bad Tc value"); + Array.Copy(this.Bytes.Buffer, this.Bytes.I, data, offset, length); + this.Bytes.I += length; + length -= length; } - - int th = this.temp[0] & 0x0f; - if (th > MaxTh || (!this.isProgressive && (th > 1))) + else { - throw new ImageFormatException("Bad Th value"); - } + 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.ProcessDefineHuffmanTablesMarkerLoop(ref this.huffmanTrees[(tc * ThRowSize) + th], ref remaining); + this.Bytes.Fill(this.InputStream); + } } } - private void ProcessDefineHuffmanTablesMarkerLoop(ref HuffmanTree huffmanTree, ref int remaining) + /// + /// Decodes the given number of bits + /// + /// The number of bits to decode. + /// The + public uint DecodeBits(int count) { - // Read nCodes and huffman.Valuess (and derive h.Length). - // nCodes[i] is the number of codes with code length i. - // h.Length is the total number of codes. - huffmanTree.Length = 0; - - int[] ncodes = new int[MaxCodeLength]; - for (int i = 0; i < ncodes.Length; i++) - { - ncodes[i] = this.temp[i + 1]; - huffmanTree.Length += ncodes[i]; - } - - if (huffmanTree.Length == 0) - { - throw new ImageFormatException("Huffman table has zero length"); - } - - if (huffmanTree.Length > MaxNCodes) - { - throw new ImageFormatException("Huffman table has excessive length"); - } - - remaining -= huffmanTree.Length + 17; - if (remaining < 0) + if (this.Bits.UnreadBits < count) { - throw new ImageFormatException("DHT has wrong length"); - } - - this.ReadFull(huffmanTree.Values, 0, huffmanTree.Length); - - // Derive the look-up table. - for (int i = 0; i < huffmanTree.Lut.Length; i++) - { - huffmanTree.Lut[i] = 0; - } - - uint x = 0, code = 0; - - for (int i = 0; i < LutSize; i++) - { - code <<= 1; - - for (int j = 0; j < ncodes[i]; j++) + ErrorCodes errorCode = this.Bits.EnsureNBits(count, this); + if (errorCode != ErrorCodes.NoError) { - // The codeLength is 1+i, so shift code by 8-(1+i) to - // calculate the high bits for every 8-bit sequence - // whose codeLength's high bits matches code. - // The high 8 bits of lutValue are the encoded value. - // The low 8 bits are 1 plus the codeLength. - byte base2 = (byte)(code << (7 - i)); - ushort lutValue = (ushort)((huffmanTree.Values[x] << 8) | (2 + i)); - - for (int k = 0; k < 1 << (7 - i); k++) - { - huffmanTree.Lut[base2 | k] = lutValue; - } - - code++; - x++; + throw new MissingFF00Exception(); } } - // Derive minCodes, maxCodes, and indices. - int c = 0, index = 0; - for (int i = 0; i < ncodes.Length; i++) - { - int nc = ncodes[i]; - if (nc == 0) - { - huffmanTree.MinCodes[i] = -1; - huffmanTree.MaxCodes[i] = -1; - huffmanTree.Indices[i] = -1; - } - else - { - huffmanTree.MinCodes[i] = c; - huffmanTree.MaxCodes[i] = c + nc - 1; - huffmanTree.Indices[i] = index; - c += nc; - index += nc; - } - - c <<= 1; - } + uint ret = this.Bits.Accumulator >> (this.Bits.UnreadBits - count); + ret = (uint)(ret & ((1 << count) - 1)); + this.Bits.UnreadBits -= count; + this.Bits.Mask >>= count; + return ret; } /// /// Returns the next Huffman-coded value from the bit-stream, decoded according to the given value. /// /// The huffman value - /// The - private byte DecodeHuffman(ref HuffmanTree huffmanTree) + /// The + public byte DecodeHuffman(ref HuffmanTree huffmanTree) { // Copy stuff to the stack: if (huffmanTree.Length == 0) @@ -648,19 +508,20 @@ namespace ImageSharp.Formats throw new ImageFormatException("Uninitialized Huffman table"); } - if (this.bits.UnreadBits < 8) + if (this.Bits.UnreadBits < 8) { - ErrorCodes errorCode = this.bits.EnsureNBits(8, this); + ErrorCodes errorCode = this.Bits.EnsureNBits(8, this); if (errorCode == ErrorCodes.NoError) { - ushort v = huffmanTree.Lut[(this.bits.Accumulator >> (this.bits.UnreadBits - LutSize)) & 0xff]; + ushort v = + huffmanTree.Lut[(this.Bits.Accumulator >> (this.Bits.UnreadBits - HuffmanTree.LutSize)) & 0xff]; if (v != 0) { byte n = (byte)((v & 0xff) - 1); - this.bits.UnreadBits -= n; - this.bits.Mask >>= n; + this.Bits.UnreadBits -= n; + this.Bits.Mask >>= n; return (byte)(v >> 8); } } @@ -671,24 +532,24 @@ namespace ImageSharp.Formats } int code = 0; - for (int i = 0; i < MaxCodeLength; i++) + for (int i = 0; i < HuffmanTree.MaxCodeLength; i++) { - if (this.bits.UnreadBits == 0) + if (this.Bits.UnreadBits == 0) { - ErrorCodes errorCode = this.bits.EnsureNBits(1, this); + ErrorCodes errorCode = this.Bits.EnsureNBits(1, this); if (errorCode != ErrorCodes.NoError) { throw new MissingFF00Exception(); } } - if ((this.bits.Accumulator & this.bits.Mask) != 0) + if ((this.Bits.Accumulator & this.Bits.Mask) != 0) { code |= 1; } - this.bits.UnreadBits--; - this.bits.Mask >>= 1; + this.Bits.UnreadBits--; + this.Bits.Mask >>= 1; if (code <= huffmanTree.MaxCodes[i]) { @@ -702,517 +563,83 @@ namespace ImageSharp.Formats } /// - /// Decodes a single bit + /// Gets the representing the channel at a given component index /// - /// The - private bool DecodeBit() + /// The component index + /// The of the channel + public JpegPixelArea GetDestinationChannel(int compIndex) { - if (this.bits.UnreadBits == 0) + if (this.ComponentCount == 1) { - ErrorCodes errorCode = this.bits.EnsureNBits(1, this); - if (errorCode != ErrorCodes.NoError) + return this.grayImage; + } + else + { + switch (compIndex) { - throw new MissingFF00Exception(); + 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"); } } - - bool ret = (this.bits.Accumulator & this.bits.Mask) != 0; - this.bits.UnreadBits--; - this.bits.Mask >>= 1; - return ret; } /// - /// Decodes the given number of bits + /// 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 number of bits to decode. - /// The - private uint DecodeBits(int count) + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) + where TColor : struct, IPackedPixel, IEquatable { - if (this.bits.UnreadBits < count) - { - ErrorCodes errorCode = this.bits.EnsureNBits(count, this); - if (errorCode != ErrorCodes.NoError) - { - throw new MissingFF00Exception(); - } - } + int ccb = cb - 128; + int ccr = cr - 128; - 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; + byte r = (byte)(y + (1.402F * ccr)).Clamp(0, 255); + byte g = (byte)(y - (0.34414F * ccb) - (0.71414F * ccr)).Clamp(0, 255); + byte b = (byte)(y + (1.772F * ccb)).Clamp(0, 255); + + packed.PackFromBytes(r, g, b, 255); } /// - /// 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. + /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header. /// - private void UnreadByteStuffedByte() + /// The pixel format. + /// The image to assign the resolution to. + private void AssignResolution(Image image) + where TColor : struct, IPackedPixel, IEquatable { - this.bytes.I -= this.bytes.UnreadableBytes; - this.bytes.UnreadableBytes = 0; - if (this.bits.UnreadBits >= 8) + if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) { - this.bits.Accumulator >>= 8; - this.bits.UnreadBits -= 8; - this.bits.Mask >>= 8; - } - } - - /// - /// 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 - private 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); - } - } - } - - /// - /// Skips the next n bytes. - /// - /// The number of bytes to ignore. - private void Skip(int count) - { - // Unread the overshot bytes, if any. - if (this.bytes.UnreadableBytes != 0) - { - if (this.bits.UnreadBits >= 8) - { - this.UnreadByteStuffedByte(); - } - - this.bytes.UnreadableBytes = 0; - } - - while (true) - { - int m = this.bytes.J - this.bytes.I; - if (m > count) - { - m = count; - } - - this.bytes.I += m; - count -= m; - if (count == 0) - { - break; - } - - this.bytes.Fill(this.inputStream); - } - } - - /// - /// Processes the Start of Frame marker. Specified in section B.2.2. - /// - /// The remaining bytes in the segment block. - private void ProcessStartOfFrameMarker(int remaining) - { - if (this.componentCount != 0) - { - throw new ImageFormatException("Multiple SOF markers"); - } - - switch (remaining) - { - 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); - - // We only support 8-bit precision. - if (this.temp[0] != 8) - { - throw new ImageFormatException("Only 8-Bit precision supported."); - } - - 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"); - } - - for (int i = 0; i < this.componentCount; 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.componentArray[i].Identifier == this.componentArray[j].Identifier) - { - throw new ImageFormatException("Repeated component identifier"); - } - } - - this.componentArray[i].Selector = this.temp[8 + (3 * i)]; - if (this.componentArray[i].Selector > MaxTq) - { - throw new ImageFormatException("Bad Tq value"); - } - - 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"); - } - - if (h == 3 || v == 3) - { - throw new ImageFormatException("Lnsupported subsampling ratio"); - } - - switch (this.componentCount) - { - case 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; - break; - - case 3: - - // 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"); - } - - break; - } - - case 1: - { - // Cb. - if (this.componentArray[0].HorizontalFactor % h != 0 - || this.componentArray[0].VerticalFactor % v != 0) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } - - break; - } - - case 2: - { - // Cr. - if (this.componentArray[1].HorizontalFactor != h - || this.componentArray[1].VerticalFactor != v) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } - - break; - } - } - - break; - - 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) - { - case 0: - if (hv != 0x11 && hv != 0x22) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } - - break; - case 1: - case 2: - if (hv != 0x11) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } - - break; - case 3: - if (this.componentArray[0].HorizontalFactor != h - || this.componentArray[0].VerticalFactor != v) - { - throw new ImageFormatException("Unsupported subsampling ratio"); - } - - break; - } - - break; - } - - this.componentArray[i].HorizontalFactor = h; - this.componentArray[i].VerticalFactor = v; - } - } - - /// - /// 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 - /// - private void ProcessDqt(int remaining) - { - while (remaining > 0) - { - bool done = false; - - remaining--; - byte x = this.ReadByte(); - byte tq = (byte)(x & 0x0f); - if (tq > MaxTq) - { - throw new ImageFormatException("Bad Tq value"); - } - - switch (x >> 4) - { - case 0: - if (remaining < Block8x8F.ScalarCount) - { - done = true; - break; - } - - remaining -= Block8x8F.ScalarCount; - this.ReadFull(this.temp, 0, Block8x8F.ScalarCount); - - for (int i = 0; i < Block8x8F.ScalarCount; i++) - { - this.quantizationTables[tq][i] = this.temp[i]; - } - - break; - case 1: - if (remaining < 2 * Block8x8F.ScalarCount) - { - done = true; - break; - } - - remaining -= 2 * Block8x8F.ScalarCount; - this.ReadFull(this.temp, 0, 2 * Block8x8F.ScalarCount); - - for (int i = 0; i < Block8x8F.ScalarCount; i++) - { - this.quantizationTables[tq][i] = (this.temp[2 * i] << 8) | this.temp[(2 * i) + 1]; - } - - break; - default: - throw new ImageFormatException("Bad Pq value"); - } - - if (done) - { - break; - } - } - - if (remaining != 0) - { - throw new ImageFormatException("DQT has wrong length"); - } - } - - /// - /// 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 - { - 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 "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); + image.HorizontalResolution = this.horizontalResolution; + image.VerticalResolution = this.verticalResolution; } } /// - /// Converts the image from the original YCCK image pixels. + /// Converts the image from the original CMYK image pixels. /// /// The pixel format. /// The image width. /// The image height. /// The image. - private void ConvertFromYcck(int width, int height, Image 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; + int scale = this.ComponentArray[0].HorizontalFactor / this.ComponentArray[1].HorizontalFactor; image.InitPixels(width, height); @@ -1222,38 +649,37 @@ namespace ImageSharp.Formats 0, height, y => + { + // TODO: Simplify + optimize + share duplicate code across converter methods + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) - { - byte yy = this.ycbcrImage.YChannel[yo + x]; - byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; - - TColor packed = default(TColor); - this.PackYcck(ref packed, yy, cb, cr, x, y); - pixels[x, y] = packed; - } - }); + byte cyan = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte magenta = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte yellow = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; + + TColor packed = default(TColor); + this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); } /// - /// Converts the image from the original CMYK image pixels. + /// Converts the image from the original grayscale image pixels. /// /// The pixel format. /// The image width. /// The image height. /// The image. - private void ConvertFromCmyk(int width, int height, Image image) + private void ConvertFromGrayScale(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()) @@ -1261,19 +687,16 @@ namespace ImageSharp.Formats Parallel.For( 0, height, + Bootstrapper.ParallelOptions, y => { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - + int yoff = this.grayImage.GetRowOffset(y); for (int x = 0; x < width; x++) { - byte cyan = this.ycbcrImage.YChannel[yo + x]; - byte magenta = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte yellow = this.ycbcrImage.CrChannel[co + (x / scale)]; + byte rgb = this.grayImage.Pixels[yoff + x]; TColor packed = default(TColor); - this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); + packed.PackFromBytes(rgb, rgb, rgb, 255); pixels[x, y] = packed; } }); @@ -1283,15 +706,16 @@ namespace ImageSharp.Formats } /// - /// Converts the image from the original grayscale image pixels. + /// Converts the image from the original RBG image pixels. /// /// The pixel format. /// The image width. - /// The image height. + /// The height. /// The image. - private void ConvertFromGrayScale(int width, int height, Image 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); using (PixelAccessor pixels = image.Lock()) @@ -1301,17 +725,22 @@ namespace ImageSharp.Formats height, Bootstrapper.ParallelOptions, y => + { + // TODO: Simplify + optimize + share duplicate code across converter methods + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) { - 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; - } - }); + byte red = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte green = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte blue = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; + + TColor packed = default(TColor); + packed.PackFromBytes(red, green, blue, 255); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); @@ -1327,7 +756,7 @@ namespace ImageSharp.Formats private void ConvertFromYCbCr(int width, int height, Image image) where TColor : struct, IPackedPixel, IEquatable { - int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + int scale = this.ComponentArray[0].HorizontalFactor / this.ComponentArray[1].HorizontalFactor; image.InitPixels(width, height); using (PixelAccessor pixels = image.Lock()) @@ -1337,37 +766,39 @@ namespace ImageSharp.Formats height, Bootstrapper.ParallelOptions, y => + { + // TODO: Simplify + optimize + share duplicate code across converter methods + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) - { - byte yy = this.ycbcrImage.YChannel[yo + x]; - byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; - - TColor packed = default(TColor); - PackYcbCr(ref packed, yy, cb, cr); - pixels[x, y] = packed; - } - }); + byte yy = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte cb = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; + + TColor packed = default(TColor); + PackYcbCr(ref packed, yy, cb, cr); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); } /// - /// Converts the image from the original RBG image pixels. + /// Converts the image from the original YCCK image pixels. /// /// The pixel format. /// The image width. - /// The height. + /// The image height. /// The image. - private void ConvertFromRGB(int width, int height, Image 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; + int scale = this.ComponentArray[0].HorizontalFactor / this.ComponentArray[1].HorizontalFactor; + image.InitPixels(width, height); using (PixelAccessor pixels = image.Lock()) @@ -1375,864 +806,648 @@ namespace ImageSharp.Formats Parallel.For( 0, height, - Bootstrapper.ParallelOptions, y => + { + // TODO: Simplify + optimize + share duplicate code across converter methods + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) - { - byte red = this.ycbcrImage.YChannel[yo + x]; - byte green = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte blue = this.ycbcrImage.CrChannel[co + (x / scale)]; - - TColor packed = default(TColor); - packed.PackFromBytes(red, green, blue, 255); - pixels[x, y] = packed; - } - }); + byte yy = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte cb = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; + + TColor packed = default(TColor); + this.PackYcck(ref packed, yy, cb, cr, x, y); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); } /// - /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header. + /// 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 image to assign the resolution to. - private void AssignResolution(Image image) + /// 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 { - if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) + // 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) { - image.HorizontalResolution = this.horizontalResolution; - image.VerticalResolution = this.verticalResolution; + 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 SOS (Start of scan marker). + /// Processes the application header containing the JFIF identifier plus extra data. /// - /// - /// 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 - /// - private void ProcessStartOfScan(int remaining) + private void ProcessApplicationHeader(int remaining) { - if (this.componentCount == 0) - { - throw new ImageFormatException("Missing SOF marker"); - } - - if (remaining < 6 || 4 + (2 * this.componentCount) < remaining || remaining % 2 != 0) + if (remaining < 5) { - throw new ImageFormatException("SOS has wrong length"); + this.Skip(remaining); + return; } - this.ReadFull(this.temp, 0, remaining); - byte scanComponentCount = this.temp[0]; - - int scanComponentCountX2 = 2 * scanComponentCount; - if (remaining != 4 + scanComponentCountX2) - { - throw new ImageFormatException("SOS length inconsistent with number of components"); - } + this.ReadFull(this.Temp, 0, 13); + remaining -= 13; - Scan[] scan = new Scan[MaxComponents]; - int totalHv = 0; + // 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'; - for (int i = 0; i < scanComponentCount; i++) + if (this.isJfif) { - this.ProcessScanImpl(i, ref scan[i], scan, ref totalHv); + this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[10] << 8)); + this.verticalResolution = (short)(this.Temp[11] + (this.Temp[12] << 8)); } - // Section B.2.3 states that if there is more than one component then the - // total H*V values in a scan must be <= 10. - if (this.componentCount > 1 && totalHv > 10) + if (remaining > 0) { - throw new ImageFormatException("Total sampling factors too large."); + this.Skip(remaining); } + } - // zigStart and zigEnd are the spectral selection bounds. - // ah and al are the successive approximation high and low values. - // The spec calls these values Ss, Se, Ah and Al. - // For progressive JPEGs, these are the two more-or-less independent - // aspects of progression. Spectral selection progression is when not - // all of a block's 64 DCT coefficients are transmitted in one pass. - // For example, three passes could transmit coefficient 0 (the DC - // component), coefficients 1-5, and coefficients 6-63, in zig-zag - // order. Successive approximation is when not all of the bits of a - // band of coefficients are transmitted in one pass. For example, - // three passes could transmit the 6 most significant bits, followed - // by the second-least significant bit, followed by the least - // significant bit. - // For baseline JPEGs, these parameters are hard-coded to 0/63/0/0. - int zigStart = 0; - int zigEnd = Block8x8F.ScalarCount - 1; - int ah = 0; - int al = 0; - - if (this.isProgressive) + /// + /// 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) { - zigStart = this.temp[1 + scanComponentCountX2]; - zigEnd = this.temp[2 + scanComponentCountX2]; - ah = this.temp[3 + scanComponentCountX2] >> 4; - al = this.temp[3 + scanComponentCountX2] & 0x0f; - - if ((zigStart == 0 && zigEnd != 0) || zigStart > zigEnd || zigEnd >= Block8x8F.ScalarCount) + if (remaining < 17) { - throw new ImageFormatException("Bad spectral selection bounds"); + throw new ImageFormatException("DHT has wrong length"); } - if (zigStart != 0 && scanComponentCount != 1) + this.ReadFull(this.Temp, 0, 17); + + int tc = this.Temp[0] >> 4; + if (tc > HuffmanTree.MaxTc) { - throw new ImageFormatException("Progressive AC coefficients for more than one component"); + throw new ImageFormatException("Bad Tc value"); } - if (ah != 0 && ah != al + 1) + int th = this.Temp[0] & 0x0f; + if (th > HuffmanTree.MaxTh || (!this.IsProgressive && (th > 1))) { - throw new ImageFormatException("Bad successive approximation values"); + throw new ImageFormatException("Bad Th value"); } - } - - // mxx and myy are the number of MCUs (Minimum Coded Units) in the image. - int h0 = this.componentArray[0].HorizontalFactor; - int v0 = this.componentArray[0].VerticalFactor; - int mxx = (this.imageWidth + (8 * h0) - 1) / (8 * h0); - int myy = (this.imageHeight + (8 * v0) - 1) / (8 * v0); - if (this.grayImage == null && this.ycbcrImage == null) - { - this.MakeImage(mxx, myy); + int huffTreeIndex = (tc * HuffmanTree.ThRowSize) + th; + this.HuffmanTrees[huffTreeIndex].ProcessDefineHuffmanTablesMarkerLoop(this, this.Temp, ref remaining); } + } - if (this.isProgressive) + /// + /// 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) { - for (int i = 0; i < scanComponentCount; i++) - { - int compIndex = scan[i].Index; - if (this.progCoeffs[compIndex] == null) - { - int size = mxx * myy * this.componentArray[compIndex].HorizontalFactor - * this.componentArray[compIndex].VerticalFactor; - - this.progCoeffs[compIndex] = new Block8x8F[size]; - } - } + throw new ImageFormatException("DRI has wrong length"); } - this.bits = default(Bits); - - int mcu = 0; - byte expectedRst = JpegConstants.Markers.RST0; - - // b is the decoded coefficients block, in natural (not zig-zag) order. - // Block b; - int[] dc = new int[MaxComponents]; - - // bx and by are the location of the current block, in units of 8x8 - // blocks: the third block in the first row has (bx, by) = (2, 0). - int bx, by, blockCount = 0; - - Block8x8F b = default(Block8x8F); - Block8x8F temp1 = default(Block8x8F); - Block8x8F temp2 = default(Block8x8F); - - UnzigData unzig = UnzigData.Create(); - - int* unzigPtr = unzig.Data; + this.ReadFull(this.Temp, 0, 2); + this.RestartInterval = ((int)this.Temp[0] << 8) + (int)this.Temp[1]; + } - for (int my = 0; my < myy; my++) + /// + /// 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 + /// + private void ProcessDqt(int remaining) + { + while (remaining > 0) { - for (int mx = 0; mx < mxx; mx++) + bool done = false; + + remaining--; + byte x = this.ReadByte(); + byte tq = (byte)(x & 0x0f); + if (tq > MaxTq) { - for (int i = 0; i < scanComponentCount; i++) - { - int compIndex = scan[i].Index; - int hi = this.componentArray[compIndex].HorizontalFactor; - int vi = this.componentArray[compIndex].VerticalFactor; + throw new ImageFormatException("Bad Tq value"); + } - for (int j = 0; j < hi * vi; j++) + switch (x >> 4) + { + case 0: + if (remaining < Block8x8F.ScalarCount) { - // The blocks are traversed one MCU at a time. For 4:2:0 chroma - // subsampling, there are four Y 8x8 blocks in every 16x16 MCU. - // For a baseline 32x16 pixel image, the Y blocks visiting order is: - // 0 1 4 5 - // 2 3 6 7 - // For progressive images, the interleaved scans (those with component count > 1) - // are traversed as above, but non-interleaved scans are traversed left - // to right, top to bottom: - // 0 1 2 3 - // 4 5 6 7 - // Only DC scans (zigStart == 0) can be interleave AC scans must have - // only one component. - // To further complicate matters, for non-interleaved scans, there is no - // data for any blocks that are inside the image at the MCU level but - // outside the image at the pixel level. For example, a 24x16 pixel 4:2:0 - // progressive image consists of two 16x16 MCUs. The interleaved scans - // will process 8 Y blocks: - // 0 1 4 5 - // 2 3 6 7 - // The non-interleaved scans will process only 6 Y blocks: - // 0 1 2 - // 3 4 5 - if (scanComponentCount != 1) - { - bx = (hi * mx) + (j % hi); - by = (vi * my) + (j / hi); - } - else - { - int q = mxx * hi; - bx = blockCount % q; - by = blockCount / q; - blockCount++; - if (bx * 8 >= this.imageWidth || by * 8 >= this.imageHeight) - { - continue; - } - } - - int qtIndex = this.componentArray[compIndex].Selector; - - // TODO: Find a way to clean up this mess - fixed (Block8x8F* qtp = &this.quantizationTables[qtIndex]) - { - // Load the previous partially decoded coefficients, if applicable. - if (this.isProgressive) - { - this.blockIndex = ((@by * mxx) * hi) + bx; - - fixed (Block8x8F* bp = &this.progCoeffs[compIndex][this.blockIndex]) - { - this.ProcessBlockImpl( - ah, - bp, - &temp1, - &temp2, - unzigPtr, - scan, - i, - zigStart, - zigEnd, - al, - dc, - compIndex, - @by, - mxx, - hi, - bx, - qtp); - } - } - else - { - b.Clear(); - this.ProcessBlockImpl( - ah, - &b, - &temp1, - &temp2, - unzigPtr, - scan, - i, - zigStart, - zigEnd, - al, - dc, - compIndex, - @by, - mxx, - hi, - bx, - qtp); - } - } + done = true; + break; } - // for j - } - - // for i - mcu++; + remaining -= Block8x8F.ScalarCount; + this.ReadFull(this.Temp, 0, Block8x8F.ScalarCount); - 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) + for (int i = 0; i < Block8x8F.ScalarCount; i++) { - throw new ImageFormatException("Bad RST marker"); + this.QuantizationTables[tq][i] = this.Temp[i]; } - expectedRst++; - if (expectedRst == JpegConstants.Markers.RST7 + 1) + break; + case 1: + if (remaining < 2 * Block8x8F.ScalarCount) { - expectedRst = JpegConstants.Markers.RST0; + done = true; + break; } - // Reset the Huffman decoder. - this.bits = default(Bits); + remaining -= 2 * Block8x8F.ScalarCount; + this.ReadFull(this.Temp, 0, 2 * Block8x8F.ScalarCount); - // Reset the DC components, as per section F.2.1.3.1. - dc = new int[MaxComponents]; + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + this.QuantizationTables[tq][i] = (this.Temp[2 * i] << 8) | this.Temp[(2 * i) + 1]; + } - // Reset the progressive decoder state, as per section G.1.2.2. - this.eobRun = 0; - } + break; + default: + throw new ImageFormatException("Bad Pq value"); } - // for mx + if (done) + { + break; + } } - // for my + if (remaining != 0) + { + throw new ImageFormatException("DQT has wrong length"); + } } - 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) + /// + /// Processes the Start of Frame marker. Specified in section B.2.2. + /// + /// The remaining bytes in the segment block. + private void ProcessStartOfFrameMarker(int remaining) { - int huffmannIdx = (AcTable * ThRowSize) + scan[i].AcTableSelector; - if (ah != 0) + if (this.ComponentCount != 0) { - this.Refine(b, ref this.huffmanTrees[huffmannIdx], unzigPtr, zigStart, zigEnd, 1 << al); + throw new ImageFormatException("Multiple SOF markers"); } - else + + switch (remaining) { - int zig = zigStart; - if (zig == 0) - { - zig++; + 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"); + } - // Decode the DC coefficient, as specified in section F.2.2.1. - byte value = this.DecodeHuffman( - ref this.huffmanTrees[(DcTable * ThRowSize) + scan[i].DcTableSelector]); - if (value > 16) - { - throw new ImageFormatException("Excessive DC component"); - } + this.ReadFull(this.Temp, 0, remaining); - int deltaDC = this.bits.ReceiveExtend(value, this); - dc[compIndex] += deltaDC; + // We only support 8-bit precision. + if (this.Temp[0] != 8) + { + throw new ImageFormatException("Only 8-Bit precision supported."); + } - // b[0] = dc[compIndex] << al; - Block8x8F.SetScalarAt(b, 0, dc[compIndex] << al); - } + 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 (zig <= zigEnd && this.eobRun > 0) - { - this.eobRun--; - } - else + for (int i = 0; i < this.ComponentCount; 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++) { - // Decode the AC coefficients, as specified in section F.2.2.2. - // Huffman huffv = ; - for (; zig <= zigEnd; zig++) + if (this.ComponentArray[i].Identifier == this.ComponentArray[j].Identifier) { - 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; - } + throw new ImageFormatException("Repeated component identifier"); } } - } - if (this.isProgressive) - { - if (zigEnd != Block8x8F.ScalarCount - 1 || al != 0) + this.ComponentArray[i].Selector = this.Temp[8 + (3 * i)]; + if (this.ComponentArray[i].Selector > MaxTq) { - // 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; + throw new ImageFormatException("Bad Tq value"); } - } - - // 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); - - byte[] dst; - int offset; - int stride; - - if (this.componentCount == 1) - { - dst = this.grayImage.Pixels; - stride = this.grayImage.Stride; - offset = this.grayImage.Offset + (8 * ((@by * this.grayImage.Stride) + bx)); - } - else - { - switch (compIndex) + byte hv = this.Temp[7 + (3 * i)]; + int h = hv >> 4; + int v = hv & 0x0f; + if (h < 1 || h > 4 || v < 1 || v > 4) { - case 0: - dst = this.ycbcrImage.YChannel; - stride = this.ycbcrImage.YStride; - offset = this.ycbcrImage.YOffset + (8 * ((@by * this.ycbcrImage.YStride) + bx)); - break; - - case 1: - dst = this.ycbcrImage.CbChannel; - stride = this.ycbcrImage.CStride; - offset = this.ycbcrImage.COffset + (8 * ((@by * this.ycbcrImage.CStride) + bx)); - break; - - case 2: - dst = this.ycbcrImage.CrChannel; - stride = this.ycbcrImage.CStride; - offset = this.ycbcrImage.COffset + (8 * ((@by * this.ycbcrImage.CStride) + bx)); - break; - - case 3: - - dst = this.blackPixels; - stride = this.blackStride; - offset = 8 * ((@by * this.blackStride) + bx); - break; - - default: - throw new ImageFormatException("Too many components"); + throw new ImageFormatException("Unsupported Luma/chroma subsampling ratio"); } - } - - // Level shift by +128, clip to [0, 255], and write to dst. - temp1->CopyColorsTo(new MutableSpan(dst, offset), stride, temp2); - } - 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) + if (h == 3 || v == 3) { - compIndex = j; + throw new ImageFormatException("Lnsupported subsampling ratio"); } - } - - if (compIndex < 0) - { - throw new ImageFormatException("Unknown component selector"); - } - - currentScan.Index = (byte)compIndex; - - this.ProcessComponentImpl(i, ref currentScan, scan, ref totalHv, ref this.componentArray[compIndex]); - } - 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) + switch (this.ComponentCount) { - throw new ImageFormatException("Repeated component selector"); - } - } + case 1: - totalHv += currentComponent.HorizontalFactor * currentComponent.VerticalFactor; + // 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; - currentScan.DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); - if (currentScan.DcTableSelector > MaxTh) - { - throw new ImageFormatException("Bad DC table selector value"); - } + case 3: + + // 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"); + } - currentScan.AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); - if (currentScan.AcTableSelector > MaxTh) - { - throw new ImageFormatException("Bad AC table selector value"); - } - } + break; + } - /// - /// Decodes a successive approximation refinement block, as specified in section G.1.2. - /// - /// The block of coefficients - /// The Huffman tree - /// Unzig ptr - /// The zig-zag start index - /// The zig-zag end index - /// The low transform offset - private void Refine(Block8x8F* b, ref HuffmanTree h, int* unzigPtr, int zigStart, int zigEnd, int delta) - { - // Refining a DC component is trivial. - if (zigStart == 0) - { - if (zigEnd != 0) - { - throw new ImageFormatException("Invalid state for zig DC component"); - } + case 1: + { + // Cb. + if (this.ComponentArray[0].HorizontalFactor % h != 0 + || this.ComponentArray[0].VerticalFactor % v != 0) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - bool bit = this.DecodeBit(); - if (bit) - { - int stuff = (int)Block8x8F.GetScalarAt(b, 0); + break; + } - // int stuff = (int)b[0]; - stuff |= delta; + case 2: + { + // Cr. + if (this.ComponentArray[1].HorizontalFactor != h + || this.ComponentArray[1].VerticalFactor != v) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - // b[0] = stuff; - Block8x8F.SetScalarAt(b, 0, stuff); - } + break; + } + } - return; - } + break; - // Refining AC components is more complicated; see sections G.1.2.2 and G.1.2.3. - int zig = zigStart; - if (this.eobRun == 0) - { - for (; zig <= zigEnd; zig++) - { - bool done = false; - int z = 0; - byte val = this.DecodeHuffman(ref h); - int val0 = val >> 4; - int val1 = val & 0x0f; + case 4: - switch (val1) - { - case 0: - if (val0 != 0x0f) - { - this.eobRun = (ushort)(1 << val0); - if (val0 != 0) + // 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) { - this.eobRun |= (ushort)this.DecodeBits(val0); + throw new ImageFormatException("Unsupported subsampling ratio"); } - done = true; - } + break; + case 1: + case 2: + if (hv != 0x11) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - break; - case 1: - z = delta; - bool bit = this.DecodeBit(); - if (!bit) - { - z = -z; - } + break; + case 3: + if (this.ComponentArray[0].HorizontalFactor != h + || this.ComponentArray[0].VerticalFactor != v) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } - break; - default: - throw new ImageFormatException("Unexpected Huffman code"); - } + break; + } - if (done) - { break; - } - - int blah = zig; - - zig = this.RefineNonZeroes(b, zig, zigEnd, val0, delta, unzigPtr); - if (zig > zigEnd) - { - throw new ImageFormatException($"Too many coefficients {zig} > {zigEnd}"); - } - - if (z != 0) - { - // b[Unzig[zig]] = z; - Block8x8F.SetScalarAt(b, unzigPtr[zig], z); - } } - } - if (this.eobRun > 0) - { - this.eobRun--; - this.RefineNonZeroes(b, zig, zigEnd, -1, delta, unzigPtr); + this.ComponentArray[i].HorizontalFactor = h; + this.ComponentArray[i].VerticalFactor = v; } } /// - /// Refines non-zero entries of b in zig-zag order. - /// If >= 0, the first zero entries are skipped over. + /// Processes the SOS (Start of scan marker). /// - /// 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 - private int RefineNonZeroes(Block8x8F* b, int zig, int zigEnd, int nz, int delta, int* unzigPtr) + /// The remaining bytes in the segment block. + /// + /// Missing SOF Marker + /// SOS has wrong length + /// + private void ProcessStartOfScan(int remaining) { - for (; zig <= zigEnd; zig++) - { - int u = unzigPtr[zig]; - float bu = Block8x8F.GetScalarAt(b, u); - - // TODO: Are the equality comparsions OK with floating point values? Isn't an epsilon value necessary? - if (bu == 0) - { - if (nz == 0) - { - break; - } - - nz--; - continue; - } - - bool bit = this.DecodeBit(); - if (!bit) - { - continue; - } - - if (bu >= 0) - { - // b[u] += delta; - Block8x8F.SetScalarAt(b, u, bu + delta); - } - else - { - // b[u] -= delta; - Block8x8F.SetScalarAt(b, u, bu - delta); - } - } - - return zig; + JpegScanDecoder scan = default(JpegScanDecoder); + JpegScanDecoder.Init(&scan, this, remaining); + this.Bits = default(Bits); + this.MakeImage(scan.XNumberOfMCUs, scan.YNumberOfMCUs); + scan.ProcessBlocks(this); } /// - /// 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.componentCount == 1) + // Unread the overshot bytes, if any. + if (this.Bytes.UnreadableBytes != 0) { - GrayImage gray = new GrayImage(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; } - YCbCrImage ycbcr = new YCbCrImage(8 * h0 * mxx, 8 * v0 * myy, ratio); - this.ycbcrImage = ycbcr.Subimage(0, 0, this.imageWidth, this.imageHeight); - - 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.blackPixels = new byte[8 * h3 * mxx * 8 * v3 * myy]; - this.blackStride = 8 * h3 * mxx; + 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.blackPixels[(yy * this.blackStride) + xx]) / 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.blackPixels[(yy * this.blackStride) + xx]) / 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 + /// The EOF (End of File exception). + /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker /// - private struct Scan + internal class EOFException : Exception { - /// - /// Gets or sets the component index. - /// - public byte Index { get; set; } - - /// - /// Gets or sets the DC table selector - /// - public byte DcTableSelector { get; set; } - - /// - /// Gets or sets the AC table selector - /// - public byte AcTableSelector { get; set; } } /// @@ -2242,14 +1457,6 @@ namespace ImageSharp.Formats { } - /// - /// The EOF (End of File exception). - /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker - /// - internal class EOFException : Exception - { - } - /// /// The short huffman data exception. /// @@ -2257,4 +1464,4 @@ namespace ImageSharp.Formats { } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs index cd0ec0af4..59dc5ce39 100644 --- a/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs @@ -420,7 +420,7 @@ namespace ImageSharp.Formats private void Encode444(PixelAccessor pixels) where TColor : struct, IPackedPixel, IEquatable { - // TODO: Need a JpegEncoderScanProcessor struct to encapsulate all this mess: + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) Block8x8F b = default(Block8x8F); Block8x8F cb = default(Block8x8F); Block8x8F cr = default(Block8x8F); @@ -759,7 +759,7 @@ namespace ImageSharp.Formats private void WriteStartOfScan(PixelAccessor pixels) where TColor : struct, IPackedPixel, IEquatable { - // TODO: This method should be the entry point for a JpegEncoderScanProcessor struct + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) // TODO: We should allow grayscale writing. this.outputStream.Write(SosHeaderYCbCr, 0, SosHeaderYCbCr.Length); @@ -786,7 +786,7 @@ namespace ImageSharp.Formats private void Encode420(PixelAccessor pixels) where TColor : struct, IPackedPixel, IEquatable { - // TODO: Need a JpegEncoderScanProcessor struct to encapsulate all this mess: + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) Block8x8F b = default(Block8x8F); BlockQuad cb = default(BlockQuad); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 57b3c56f8..6736548e6 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -1,4 +1,9 @@ -// Uncomment this to turn unit tests into benchmarks: +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// Uncomment this to turn unit tests into benchmarks: //#define BENCHMARKING // ReSharper disable InconsistentNaming diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs index 6c05b3ef2..53c44d836 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs @@ -1,4 +1,9 @@ -using System; +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -83,13 +88,13 @@ namespace ImageSharp.Tests byte[] bytes = File.ReadAllBytes(path); this.Measure( - 40, + 100, () => { Image img = new Image(bytes); }, $"Decode {fileName}"); - + } //[Theory] // Benchmark, enable manually @@ -128,7 +133,7 @@ namespace ImageSharp.Tests for (int j = 0; j < 10; j++) { Vector4 v = new Vector4(i/10f, j/10f, 0, 1); - + TColor color = default(TColor); color.PackFromVector4(v); @@ -166,11 +171,11 @@ namespace ImageSharp.Tests [Theory] [WithMemberFactory(nameof(CreateTestImage), PixelTypes.Color | PixelTypes.StandardImageClass | PixelTypes.Argb)] - public void CopyStretchedRGBTo_WithOffset(TestImageProvider provider) + public void CopyStretchedRGBTo_WithOffset(TestImageProvider provider) where TColor : struct, IPackedPixel, IEquatable { var src = provider.GetImage(); - + PixelArea area = new PixelArea(8, 8, ComponentOrder.Xyz); var dest = provider.Factory.CreateImage(8, 8); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs index a9c87144a..6aed0d3fa 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs @@ -1,4 +1,9 @@ -// ReSharper disable InconsistentNaming +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming namespace ImageSharp.Tests { @@ -8,7 +13,7 @@ namespace ImageSharp.Tests using ImageSharp.Formats; using ImageSharp.Formats.Jpg; - + /// /// This class contains simplified (unefficient) reference implementations to produce verification data for unit tests /// Floating point DCT code Ported from https://github.com/norishigefukushima/dct_simd @@ -48,6 +53,9 @@ namespace ImageSharp.Tests } } + /// + /// The "original" libjpeg/golang based DCT implementation is used as reference implementation for tests. + /// public static class IntegerReferenceDCT { private const int fix_0_298631336 = 2446; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.cs index dcccc58be..66ddeabc0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.cs @@ -1,4 +1,9 @@ -// ReSharper disable InconsistentNaming +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming namespace ImageSharp.Tests.Formats.Jpg { using System.Numerics; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs b/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs index 74c6772b7..c92c6aa9a 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs @@ -1,4 +1,9 @@ -using System.Text; +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +using System.Text; using ImageSharp.Formats; using Xunit.Abstractions; // ReSharper disable InconsistentNaming @@ -15,7 +20,7 @@ namespace ImageSharp.Tests /// /// Utility class to measure the execution of an operation. /// - public class MeasureFixture + public class MeasureFixture : TestBase { protected bool EnablePrinting = true; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs new file mode 100644 index 000000000..b90973a9c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs @@ -0,0 +1,65 @@ +namespace ImageSharp.Tests +{ + using ImageSharp.Formats.Jpg; + + using Xunit; + using Xunit.Abstractions; + + public class YCbCrImageTests + { + public YCbCrImageTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + private void PrintChannel(string name, JpegPixelArea channel) + { + this.Output.WriteLine($"{name}: Stride={channel.Stride}"); + } + + [Theory] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410, 4, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411, 4, 1)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420, 2, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422, 2, 1)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440, 1, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444, 1, 1)] + public void CalculateChrominanceSize(int ratioValue, int expectedDivX, int expectedDivY) + { + YCbCrImage.YCbCrSubsampleRatio ratio = (YCbCrImage.YCbCrSubsampleRatio)ratioValue; + + //this.Output.WriteLine($"RATIO: {ratio}"); + Size size = YCbCrImage.CalculateChrominanceSize(400, 400, ratio); + //this.Output.WriteLine($"Ch Size: {size}"); + + Assert.Equal(new Size(400/expectedDivX, 400/expectedDivY), size); + } + + [Theory] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410, 4)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411, 4)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440, 1)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444, 1)] + public void Create(int ratioValue, int expectedCStrideDiv) + { + YCbCrImage.YCbCrSubsampleRatio ratio = (YCbCrImage.YCbCrSubsampleRatio)ratioValue; + + this.Output.WriteLine($"RATIO: {ratio}"); + + var img = new YCbCrImage(400, 400, ratio); + + //this.PrintChannel("Y", img.YChannel); + //this.PrintChannel("Cb", img.CbChannel); + //this.PrintChannel("Cr", img.CrChannel); + + Assert.Equal(img.YChannel.Stride, 400); + Assert.Equal(img.CbChannel.Stride, 400 / expectedCStrideDiv); + Assert.Equal(img.CrChannel.Stride, 400 / expectedCStrideDiv); + } + + } +} \ No newline at end of file