Browse Source

Merge pull request #62 from antonfirsov/jpeg-optimizations

Jpeg decoder refactor & code cleanup
pull/65/head
James Jackson-South 9 years ago
committed by GitHub
parent
commit
03bb6bbf36
  1. 1
      ImageSharp.sln.DotSettings
  2. 101
      src/ImageSharp/Formats/Jpg/Components/Decoder/GrayImage.cs
  3. 173
      src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs
  4. 135
      src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs
  5. 755
      src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.cs
  6. 25
      src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.md
  7. 31
      src/ImageSharp/Formats/Jpg/Components/Decoder/Scan.cs
  8. 190
      src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs
  9. 2349
      src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs
  10. 6
      src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs
  11. 7
      tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs
  12. 17
      tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs
  13. 12
      tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs
  14. 7
      tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.cs
  15. 9
      tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs
  16. 65
      tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs

1
ImageSharp.sln.DotSettings

@ -345,6 +345,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FDCT/@EntryIndexedValue">FDCT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IDCT/@EntryIndexedValue">IDCT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JPEG/@EntryIndexedValue">JPEG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MCU/@EntryIndexedValue">MCU</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PNG/@EntryIndexedValue">PNG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RGB/@EntryIndexedValue">RGB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RLE/@EntryIndexedValue">RLE</s:String>

101
src/ImageSharp/Formats/Jpg/Components/Decoder/GrayImage.cs

@ -1,101 +0,0 @@
// <copyright file="GrayImage.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
/// <summary>
/// Represents a grayscale image
/// </summary>
internal class GrayImage
{
/// <summary>
/// Initializes a new instance of the <see cref="GrayImage"/> class.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
public GrayImage(int width, int height)
{
this.Width = width;
this.Height = height;
this.Pixels = new byte[width * height];
this.Stride = width;
this.Offset = 0;
}
/// <summary>
/// Prevents a default instance of the <see cref="GrayImage"/> class from being created.
/// </summary>
private GrayImage()
{
}
/// <summary>
/// Gets or sets the pixels.
/// </summary>
public byte[] Pixels { get; set; }
/// <summary>
/// Gets or sets the stride.
/// </summary>
public int Stride { get; set; }
/// <summary>
/// Gets or sets the horizontal position.
/// </summary>
public int X { get; set; }
/// <summary>
/// Gets or sets the vertical position.
/// </summary>
public int Y { get; set; }
/// <summary>
/// Gets or sets the width.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the height.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets the offset
/// </summary>
public int Offset { get; set; }
/// <summary>
/// Gets an image made up of a subset of the originals pixels.
/// </summary>
/// <param name="x">The x-coordinate of the image.</param>
/// <param name="y">The y-coordinate of the image.</param>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>
/// The <see cref="GrayImage"/>.
/// </returns>
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
};
}
/// <summary>
/// Gets the row offset at the given position
/// </summary>
/// <param name="y">The y-coordinate of the image.</param>
/// <returns>The <see cref="int"/></returns>
public int GetRowOffset(int y)
{
return this.Offset + (y * this.Stride);
}
}
}

173
src/ImageSharp/Formats/Jpg/Components/Decoder/HuffmanTree.cs

@ -12,6 +12,41 @@ namespace ImageSharp.Formats.Jpg
/// </summary>
internal struct HuffmanTree : IDisposable
{
/// <summary>
/// The maximum (inclusive) number of codes in a Huffman tree.
/// </summary>
public const int MaxNCodes = 256;
/// <summary>
/// The maximum (inclusive) number of bits in a Huffman code.
/// </summary>
public const int MaxCodeLength = 16;
/// <summary>
/// The maximum number of Huffman table classes
/// </summary>
public const int MaxTc = 1;
/// <summary>
/// The maximum number of Huffman table identifiers
/// </summary>
public const int MaxTh = 3;
/// <summary>
/// Row size of the Huffman table
/// </summary>
public const int ThRowSize = MaxTh + 1;
/// <summary>
/// Number of Hufman Trees in the Huffman table
/// </summary>
public const int NumberOfTrees = (MaxTc + 1) * (MaxTh + 1);
/// <summary>
/// The log-2 size of the Huffman decoder's look-up table.
/// </summary>
public const int LutSize = 8;
/// <summary>
/// Gets or sets the number of codes in the tree.
/// </summary>
@ -47,25 +82,28 @@ namespace ImageSharp.Formats.Jpg
/// </summary>
public int[] Indices;
private static readonly ArrayPool<ushort> UshortBuffer = ArrayPool<ushort>.Create(1 << JpegDecoderCore.LutSize, 50);
private static readonly ArrayPool<ushort> UshortBuffer = ArrayPool<ushort>.Create(1 << LutSize, 50);
private static readonly ArrayPool<byte> ByteBuffer = ArrayPool<byte>.Create(JpegDecoderCore.MaxNCodes, 50);
private static readonly ArrayPool<byte> ByteBuffer = ArrayPool<byte>.Create(MaxNCodes, 50);
private static readonly ArrayPool<int> IntBuffer = ArrayPool<int>.Create(JpegDecoderCore.MaxCodeLength, 50);
private static readonly ArrayPool<int> IntBuffer = ArrayPool<int>.Create(MaxCodeLength, 50);
/// <summary>
/// Initializes the Huffman tree
/// Creates and initializes an array of <see cref="HuffmanTree" /> instances of size <see cref="NumberOfTrees" />
/// </summary>
/// <param name="lutSize">Lut size</param>
/// <param name="maxNCodes">Max N codes</param>
/// <param name="maxCodeLength">Max code length</param>
public void Init(int lutSize, int maxNCodes, int maxCodeLength)
/// <returns>An array of <see cref="HuffmanTree" /> instances representing the Huffman tables</returns>
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;
}
/// <summary>
@ -79,5 +117,114 @@ namespace ImageSharp.Formats.Jpg
IntBuffer.Return(this.MaxCodes, true);
IntBuffer.Return(this.Indices, true);
}
/// <summary>
/// Internal part of the DHT processor, whatever does it mean
/// </summary>
/// <param name="decoder">The decoder instance</param>
/// <param name="defineHuffmanTablesData">The temporal buffer that holds the data that has been read from the Jpeg stream</param>
/// <param name="remaining">Remaining bits</param>
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;
}
}
/// <summary>
/// Initializes the Huffman tree
/// </summary>
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);
}
}
}

135
src/ImageSharp/Formats/Jpg/Components/Decoder/JpegPixelArea.cs

@ -0,0 +1,135 @@
// <copyright file="JpegPixelArea.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System.Buffers;
using System.Runtime.CompilerServices;
/// <summary>
/// Represents an area of a Jpeg subimage (channel)
/// </summary>
internal struct JpegPixelArea
{
/// <summary>
/// Initializes a new instance of the <see cref="JpegPixelArea" /> struct from existing data.
/// </summary>
/// <param name="pixels">The pixel array</param>
/// <param name="striede">The stride</param>
/// <param name="offset">The offset</param>
public JpegPixelArea(byte[] pixels, int striede, int offset)
{
this.Stride = striede;
this.Pixels = pixels;
this.Offset = offset;
}
/// <summary>
/// Gets the pixels.
/// </summary>
public byte[] Pixels { get; private set; }
/// <summary>
/// Gets a value indicating whether the instance has been initalized. (Is not default(JpegPixelArea))
/// </summary>
public bool IsInitialized => this.Pixels != null;
/// <summary>
/// Gets or the stride.
/// </summary>
public int Stride { get; }
/// <summary>
/// Gets or the offset.
/// </summary>
public int Offset { get; }
/// <summary>
/// Gets a <see cref="MutableSpan{T}" /> of bytes to the pixel area
/// </summary>
public MutableSpan<byte> Span => new MutableSpan<byte>(this.Pixels, this.Offset);
private static ArrayPool<byte> BytePool => ArrayPool<byte>.Shared;
/// <summary>
/// Returns the pixel at (x, y)
/// </summary>
/// <param name="x">The x index</param>
/// <param name="y">The y index</param>
/// <returns>The pixel value</returns>
public byte this[int x, int y]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return this.Pixels[(y * this.Stride) + x];
}
}
/// <summary>
/// Creates a new instance of the <see cref="JpegPixelArea" /> struct.
/// Pixel array will be taken from a pool, this instance will be the owner of it's pixel data, therefore
/// <see cref="ReturnPooled" /> should be called when the instance is no longer needed.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>A <see cref="JpegPixelArea" /> with pooled data</returns>
public static JpegPixelArea CreatePooled(int width, int height)
{
int size = width * height;
var pixels = BytePool.Rent(size);
return new JpegPixelArea(pixels, width, 0);
}
/// <summary>
/// Returns <see cref="Pixels" /> to the pool
/// </summary>
public void ReturnPooled()
{
if (this.Pixels == null)
{
return;
}
BytePool.Return(this.Pixels);
this.Pixels = null;
}
/// <summary>
/// Gets the subarea that belongs to the Block8x8 defined by block indices
/// </summary>
/// <param name="bx">The block X index</param>
/// <param name="by">The block Y index</param>
/// <returns>The subarea offseted by block indices</returns>
public JpegPixelArea GetOffsetedSubAreaForBlock(int bx, int by)
{
int offset = this.Offset + (8 * ((by * this.Stride) + bx));
return new JpegPixelArea(this.Pixels, this.Stride, offset);
}
/// <summary>
/// Gets the row offset at the given position
/// </summary>
/// <param name="y">The y-coordinate of the image.</param>
/// <returns>The <see cref="int" /></returns>
public int GetRowOffset(int y)
{
return this.Offset + (y * this.Stride);
}
/// <summary>
/// Load values to the pixel area from the given <see cref="Block8x8F" />.
/// Level shift [-128.0, 128.0] floating point color values by +128, clip them to [0, 255], and convert them to
/// <see cref="byte" /> values
/// </summary>
/// <param name="block">The block holding the color values</param>
/// <param name="temp">Temporal block provided by the caller</param>
[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<byte>(this.Pixels, this.Offset), this.Stride, temp);
}
}
}

755
src/ImageSharp/Formats/Jpg/Components/Decoder/JpegScanDecoder.cs

@ -0,0 +1,755 @@
// <copyright file="JpegScanDecoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// ReSharper disable InconsistentNaming
namespace ImageSharp.Formats.Jpg
{
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
/// <summary>
/// Encapsulates the impementation of Jpeg SOS decoder.
/// See JpegScanDecoder.md!
/// </summary>
internal unsafe struct JpegScanDecoder
{
/// <summary>
/// Number of MCU-s (Minimum Coded Units) in the image along the X axis
/// </summary>
public int XNumberOfMCUs;
/// <summary>
/// Number of MCU-s (Minimum Coded Units) in the image along the Y axis
/// </summary>
public int YNumberOfMCUs;
/// <summary>
/// The AC table index
/// </summary>
private const int AcTableIndex = 1;
/// <summary>
/// The DC table index
/// </summary>
private const int DcTableIndex = 0;
/// <summary>
/// X coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0))
/// </summary>
private int bx;
/// <summary>
/// Y coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0))
/// </summary>
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.
/// <summary>
/// Start index of the zig-zag selection bound
/// </summary>
private int zigStart;
/// <summary>
/// End index of the zig-zag selection bound
/// </summary>
private int zigEnd;
/// <summary>
/// Successive approximation high value
/// </summary>
private int ah;
/// <summary>
/// Successive approximation high and low value
/// </summary>
private int al;
/// <summary>
/// The number of component scans
/// </summary>
private int componentScanCount;
/// <summary>
/// End-of-Band run, specified in section G.1.2.2.
/// </summary>
private ushort eobRun;
/// <summary>
/// The <see cref="ComputationData"/> buffer
/// </summary>
private ComputationData data;
/// <summary>
/// Pointers to elements of <see cref="data"/>
/// </summary>
private DataPointers pointers;
/// <summary>
/// Initializes the default instance after creation.
/// </summary>
/// <param name="p">Pointer to <see cref="JpegScanDecoder"/> on the stack</param>
/// <param name="decoder">The <see cref="JpegDecoderCore"/> instance</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
public static void Init(JpegScanDecoder* p, JpegDecoderCore decoder, int remaining)
{
p->data = ComputationData.Create();
p->pointers = new DataPointers(&p->data);
p->InitImpl(decoder, remaining);
}
/// <summary>
/// Reads the blocks from the <see cref="JpegDecoderCore"/>-s stream, and processes them into the corresponding <see cref="JpegPixelArea"/> instances.
/// </summary>
/// <param name="decoder">The <see cref="JpegDecoderCore"/> instance</param>
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);
}
/// <summary>
/// The implementation part of <see cref="Init"/> as an instance method.
/// </summary>
/// <param name="decoder">The <see cref="JpegDecoderCore"/></param>
/// <param name="remaining">The remaining bytes</param>
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];
}
}
}
}
/// <summary>
/// Process the current block at (<see cref="bx"/>, <see cref="by"/>)
/// </summary>
/// <param name="decoder">The decoder</param>
/// <param name="i">The index of the scan</param>
/// <param name="compIndex">The component index</param>
/// <param name="hi">Horizontal sampling factor at the given component index</param>
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");
}
}
/// <summary>
/// Decodes a successive approximation refinement block, as specified in section G.1.2.
/// </summary>
/// <param name="decoder">The decoder instance</param>
/// <param name="h">The Huffman tree</param>
/// <param name="delta">The low transform offset</param>
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);
}
}
/// <summary>
/// Refines non-zero entries of b in zig-zag order.
/// If <paramref name="nz" /> >= 0, the first <paramref name="nz" /> zero entries are skipped over.
/// </summary>
/// <param name="decoder">The decoder</param>
/// <param name="zig">The zig-zag start index</param>
/// <param name="nz">The non-zero entry</param>
/// <param name="delta">The low transform offset</param>
/// <returns>The <see cref="int" /></returns>
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;
}
/// <summary>
/// Holds the "large" data blocks needed for computations
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ComputationData
{
/// <summary>
/// The main input block
/// </summary>
public Block8x8F Block;
/// <summary>
/// Temporal block 1 to store intermediate and/or final computation results
/// </summary>
public Block8x8F Temp1;
/// <summary>
/// Temporal block 2 to store intermediate and/or final computation results
/// </summary>
public Block8x8F Temp2;
/// <summary>
/// The quantization table as <see cref="Block8x8F"/>
/// </summary>
public Block8x8F QuantiazationTable;
/// <summary>
/// The jpeg unzig data
/// </summary>
public UnzigData Unzig;
/// <summary>
/// The no-idea-what's this data
/// </summary>
public fixed byte ScanData[3 * JpegDecoderCore.MaxComponents];
/// <summary>
/// The DC component values
/// </summary>
public fixed int Dc[JpegDecoderCore.MaxComponents];
/// <summary>
/// Creates and initializes a new <see cref="ComputationData"/> instance
/// </summary>
/// <returns>The <see cref="ComputationData"/></returns>
public static ComputationData Create()
{
ComputationData data = default(ComputationData);
data.Unzig = UnzigData.Create();
return data;
}
}
/// <summary>
/// Contains pointers to the memory regions of <see cref="ComputationData"/> so they can be easily passed around to pointer based utility methods of <see cref="Block8x8F"/>
/// </summary>
public struct DataPointers
{
/// <summary>
/// Pointer to <see cref="ComputationData.Block"/>
/// </summary>
public Block8x8F* Block;
/// <summary>
/// Pointer to <see cref="ComputationData.Temp1"/>
/// </summary>
public Block8x8F* Temp1;
/// <summary>
/// Pointer to <see cref="ComputationData.Temp2"/>
/// </summary>
public Block8x8F* Temp2;
/// <summary>
/// Pointer to <see cref="ComputationData.QuantiazationTable"/>
/// </summary>
public Block8x8F* QuantiazationTable;
/// <summary>
/// Pointer to <see cref="ComputationData.Unzig"/> as int*
/// </summary>
public int* Unzig;
/// <summary>
/// Pointer to <see cref="ComputationData.ScanData"/> as Scan*
/// </summary>
public Scan* Scan;
/// <summary>
/// Pointer to <see cref="ComputationData.Dc"/>
/// </summary>
public int* Dc;
/// <summary>
/// Initializes a new instance of the <see cref="DataPointers" /> struct.
/// </summary>
/// <param name="basePtr">The pointer pointing to <see cref="ComputationData"/></param>
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;
}
}
}
}

25
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`

31
src/ImageSharp/Formats/Jpg/Components/Decoder/Scan.cs

@ -0,0 +1,31 @@
// <copyright file="Scan.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System.Runtime.InteropServices;
/// <summary>
/// Represents a component scan
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct Scan
{
/// <summary>
/// Gets or sets the component index.
/// </summary>
public byte Index;
/// <summary>
/// Gets or sets the DC table selector
/// </summary>
public byte DcTableSelector;
/// <summary>
/// Gets or sets the AC table selector
/// </summary>
public byte AcTableSelector;
}
}

190
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.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System;
using System.Buffers;
/// <summary>
/// Represents an image made up of three color components (luminance, blue chroma, red chroma)
/// </summary>
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
/// <summary>
/// Gets the luminance components channel as <see cref="JpegPixelArea" />.
/// </summary>
public JpegPixelArea YChannel;
/// <summary>
/// Gets the blue chroma components channel as <see cref="JpegPixelArea" />.
/// </summary>
public JpegPixelArea CbChannel;
/// <summary>
/// Gets an offseted <see cref="JpegPixelArea" /> to the Cr channel
/// </summary>
public JpegPixelArea CrChannel;
#pragma warning restore SA1401
/// <summary>
/// Initializes a new instance of the <see cref="YCbCrImage"/> class.
/// Initializes a new instance of the <see cref="YCbCrImage" /> class.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="ratio">The ratio.</param>
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;
/// <summary>
/// Prevents a default instance of the <see cref="YCbCrImage"/> class from being created.
/// </summary>
private YCbCrImage()
{
this.YChannel = JpegPixelArea.CreatePooled(width, height);
this.CbChannel = JpegPixelArea.CreatePooled(cSize.Width, cSize.Height);
this.CrChannel = JpegPixelArea.CreatePooled(cSize.Width, cSize.Height);
}
/// <summary>
@ -76,60 +86,15 @@ namespace ImageSharp.Formats.Jpg
}
/// <summary>
/// Gets or sets the luminance components channel.
/// </summary>
public byte[] YChannel { get; set; }
/// <summary>
/// Gets or sets the blue chroma components channel.
/// Gets the Y slice index delta between vertically adjacent pixels.
/// </summary>
public byte[] CbChannel { get; set; }
public int YStride { get; }
/// <summary>
/// Gets or sets the red chroma components channel.
/// </summary>
public byte[] CrChannel { get; set; }
/// <summary>
/// Gets or sets the Y slice index delta between vertically adjacent pixels.
/// </summary>
public int YStride { get; set; }
/// <summary>
/// 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.
/// </summary>
public int CStride { get; set; }
/// <summary>
/// Gets or sets the index of the first luminance element.
/// </summary>
public int YOffset { get; set; }
/// <summary>
/// Gets or sets the index of the first element of red or blue chroma.
/// </summary>
public int COffset { get; set; }
/// <summary>
/// Gets or sets the horizontal position.
/// </summary>
public int X { get; set; }
/// <summary>
/// Gets or sets the vertical position.
/// </summary>
public int Y { get; set; }
/// <summary>
/// Gets or sets the width.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the height.
/// </summary>
public int Height { get; set; }
public int CStride { get; }
/// <summary>
/// Gets or sets the subsampling ratio.
@ -137,43 +102,13 @@ namespace ImageSharp.Formats.Jpg
public YCbCrSubsampleRatio Ratio { get; set; }
/// <summary>
/// Gets an image made up of a subset of the originals pixels.
/// Disposes the <see cref="YCbCrImage" /> returning rented arrays to the pools.
/// </summary>
/// <param name="x">The x-coordinate of the image.</param>
/// <param name="y">The y-coordinate of the image.</param>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>
/// The <see cref="YCbCrImage"/>.
/// </returns>
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;
}
/// <summary>
/// Returns the offset of the first luminance component at the given row
/// </summary>
/// <param name="y">The row number.</param>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
public int GetRowYOffset(int y)
{
return y * this.YStride;
this.YChannel.ReturnPooled();
this.CbChannel.ReturnPooled();
this.CrChannel.ReturnPooled();
}
/// <summary>
@ -181,7 +116,7 @@ namespace ImageSharp.Formats.Jpg
/// </summary>
/// <param name="y">The row number.</param>
/// <returns>
/// The <see cref="int"/>.
/// The <see cref="int" />.
/// </returns>
public int GetRowCOffset(int y)
{
@ -202,45 +137,46 @@ namespace ImageSharp.Formats.Jpg
}
}
/// <summary>
/// Returns the offset of the first luminance component at the given row
/// </summary>
/// <param name="y">The row number.</param>
/// <returns>
/// The <see cref="int" />.
/// </returns>
public int GetRowYOffset(int y)
{
return y * this.YStride;
}
/// <summary>
/// Returns the height and width of the chroma components
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="ratio">The subsampling ratio.</param>
/// <param name="chromaWidth">The chroma width.</param>
/// <param name="chromaHeight">The chroma height.</param>
private static void YCbCrSize(int width, int height, YCbCrSubsampleRatio ratio, out int chromaWidth, out int chromaHeight)
/// <returns>The <see cref="Size"/> of the chrominance channel</returns>
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);
}
}
}
}
}

2349
src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs

File diff suppressed because it is too large

6
src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs

@ -420,7 +420,7 @@ namespace ImageSharp.Formats
private void Encode444<TColor>(PixelAccessor<TColor> pixels)
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
// TODO: Need a JpegEncoderScanProcessor<TColor> struct to encapsulate all this mess:
// TODO: Need a JpegScanEncoder<TColor> 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<TColor>(PixelAccessor<TColor> pixels)
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
// TODO: This method should be the entry point for a JpegEncoderScanProcessor<TColor> struct
// TODO: Need a JpegScanEncoder<TColor> 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<TColor>(PixelAccessor<TColor> pixels)
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
// TODO: Need a JpegEncoderScanProcessor<TColor> struct to encapsulate all this mess:
// TODO: Need a JpegScanEncoder<TColor> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
Block8x8F b = default(Block8x8F);
BlockQuad cb = default(BlockQuad);

7
tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs

@ -1,4 +1,9 @@
// Uncomment this to turn unit tests into benchmarks:
// <copyright file="Block8x8FTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// Uncomment this to turn unit tests into benchmarks:
//#define BENCHMARKING
// ReSharper disable InconsistentNaming

17
tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs

@ -1,4 +1,9 @@
using System;
// <copyright file="JpegTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
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<TColor>(TestImageProvider<TColor> provider)
public void CopyStretchedRGBTo_WithOffset<TColor>(TestImageProvider<TColor> provider)
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
var src = provider.GetImage();
PixelArea<TColor> area = new PixelArea<TColor>(8, 8, ComponentOrder.Xyz);
var dest = provider.Factory.CreateImage(8, 8);

12
tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs

@ -1,4 +1,9 @@
// ReSharper disable InconsistentNaming
// <copyright file="ReferenceImplementations.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// ReSharper disable InconsistentNaming
namespace ImageSharp.Tests
{
@ -8,7 +13,7 @@ namespace ImageSharp.Tests
using ImageSharp.Formats;
using ImageSharp.Formats.Jpg;
/// <summary>
/// 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
}
}
/// <summary>
/// The "original" libjpeg/golang based DCT implementation is used as reference implementation for tests.
/// </summary>
public static class IntegerReferenceDCT
{
private const int fix_0_298631336 = 2446;

7
tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.cs

@ -1,4 +1,9 @@
// ReSharper disable InconsistentNaming
// <copyright file="ReferenceImplementationsTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// ReSharper disable InconsistentNaming
namespace ImageSharp.Tests.Formats.Jpg
{
using System.Numerics;

9
tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs

@ -1,4 +1,9 @@
using System.Text;
// <copyright file="UtilityTestClassBase.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
using System.Text;
using ImageSharp.Formats;
using Xunit.Abstractions;
// ReSharper disable InconsistentNaming
@ -15,7 +20,7 @@ namespace ImageSharp.Tests
/// <summary>
/// Utility class to measure the execution of an operation.
/// </summary>
public class MeasureFixture
public class MeasureFixture : TestBase
{
protected bool EnablePrinting = true;

65
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);
}
}
}
Loading…
Cancel
Save