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