mirror of https://github.com/SixLabors/ImageSharp
149 changed files with 7312 additions and 5064 deletions
@ -0,0 +1,306 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Text; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a Jpeg block with <see cref="short"/> coefficiens.
|
|||
/// </summary>
|
|||
// ReSharper disable once InconsistentNaming
|
|||
internal unsafe struct Block8x8 : IEquatable<Block8x8> |
|||
{ |
|||
/// <summary>
|
|||
/// A number of scalar coefficients in a <see cref="Block8x8F"/>
|
|||
/// </summary>
|
|||
public const int Size = 64; |
|||
|
|||
/// <summary>
|
|||
/// A fixed size buffer holding the values.
|
|||
/// See: <see>
|
|||
/// <cref>https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/unsafe-code-pointers/fixed-size-buffers</cref>
|
|||
/// </see>
|
|||
/// </summary>
|
|||
private fixed short data[Size]; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Block8x8"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="coefficients">A <see cref="Span{T}"/> of coefficients</param>
|
|||
public Block8x8(Span<short> coefficients) |
|||
{ |
|||
ref byte selfRef = ref Unsafe.As<Block8x8, byte>(ref this); |
|||
ref byte sourceRef = ref coefficients.NonPortableCast<short, byte>().DangerousGetPinnableReference(); |
|||
Unsafe.CopyBlock(ref selfRef, ref sourceRef, Size * sizeof(short)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a <see cref="short"/> value at the given index
|
|||
/// </summary>
|
|||
/// <param name="idx">The index</param>
|
|||
/// <returns>The value</returns>
|
|||
public short this[int idx] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
GuardBlockIndex(idx); |
|||
ref short selfRef = ref Unsafe.As<Block8x8, short>(ref this); |
|||
return Unsafe.Add(ref selfRef, idx); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
set |
|||
{ |
|||
GuardBlockIndex(idx); |
|||
ref short selfRef = ref Unsafe.As<Block8x8, short>(ref this); |
|||
Unsafe.Add(ref selfRef, idx) = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value in a row+coulumn of the 8x8 block
|
|||
/// </summary>
|
|||
/// <param name="x">The x position index in the row</param>
|
|||
/// <param name="y">The column index</param>
|
|||
/// <returns>The value</returns>
|
|||
public short this[int x, int y] |
|||
{ |
|||
get => this[(y * 8) + x]; |
|||
set => this[(y * 8) + x] = value; |
|||
} |
|||
|
|||
public static bool operator ==(Block8x8 left, Block8x8 right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
public static bool operator !=(Block8x8 left, Block8x8 right) |
|||
{ |
|||
return !left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Multiply all elements by a given <see cref="int"/>
|
|||
/// </summary>
|
|||
public static Block8x8 operator *(Block8x8 block, int value) |
|||
{ |
|||
Block8x8 result = block; |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
int val = result[i]; |
|||
val *= value; |
|||
result[i] = (short)val; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Divide all elements by a given <see cref="int"/>
|
|||
/// </summary>
|
|||
public static Block8x8 operator /(Block8x8 block, int value) |
|||
{ |
|||
Block8x8 result = block; |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
int val = result[i]; |
|||
val /= value; |
|||
result[i] = (short)val; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add an <see cref="int"/> to all elements
|
|||
/// </summary>
|
|||
public static Block8x8 operator +(Block8x8 block, int value) |
|||
{ |
|||
Block8x8 result = block; |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
int val = result[i]; |
|||
val += value; |
|||
result[i] = (short)val; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Subtract an <see cref="int"/> from all elements
|
|||
/// </summary>
|
|||
public static Block8x8 operator -(Block8x8 block, int value) |
|||
{ |
|||
Block8x8 result = block; |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
int val = result[i]; |
|||
val -= value; |
|||
result[i] = (short)val; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Pointer-based "Indexer" (getter part)
|
|||
/// </summary>
|
|||
/// <param name="blockPtr">Block pointer</param>
|
|||
/// <param name="idx">Index</param>
|
|||
/// <returns>The scaleVec value at the specified index</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static short GetScalarAt(Block8x8* blockPtr, int idx) |
|||
{ |
|||
GuardBlockIndex(idx); |
|||
|
|||
short* fp = blockPtr->data; |
|||
return fp[idx]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Pointer-based "Indexer" (setter part)
|
|||
/// </summary>
|
|||
/// <param name="blockPtr">Block pointer</param>
|
|||
/// <param name="idx">Index</param>
|
|||
/// <param name="value">Value</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void SetScalarAt(Block8x8* blockPtr, int idx, short value) |
|||
{ |
|||
GuardBlockIndex(idx); |
|||
|
|||
short* fp = blockPtr->data; |
|||
fp[idx] = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convert into <see cref="Block8x8F"/>
|
|||
/// </summary>
|
|||
public Block8x8F AsFloatBlock() |
|||
{ |
|||
// TODO: Optimize this
|
|||
var result = default(Block8x8F); |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
result[i] = this[i]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copy all elements to an array of <see cref="short"/>.
|
|||
/// </summary>
|
|||
public short[] ToArray() |
|||
{ |
|||
short[] result = new short[Size]; |
|||
this.CopyTo(result); |
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copy elements into 'destination' Span of <see cref="short"/> values
|
|||
/// </summary>
|
|||
public void CopyTo(Span<short> destination) |
|||
{ |
|||
ref byte selfRef = ref Unsafe.As<Block8x8, byte>(ref this); |
|||
ref byte destRef = ref destination.NonPortableCast<short, byte>().DangerousGetPinnableReference(); |
|||
Unsafe.CopyBlock(ref destRef, ref selfRef, Size * sizeof(short)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copy elements into 'destination' Span of <see cref="int"/> values
|
|||
/// </summary>
|
|||
public void CopyTo(Span<int> destination) |
|||
{ |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
destination[i] = this[i]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Cast and copy <see cref="Size"/> <see cref="int"/>-s from the beginning of 'source' span.
|
|||
/// </summary>
|
|||
public void LoadFrom(Span<int> source) |
|||
{ |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
this[i] = (short)source[i]; |
|||
} |
|||
} |
|||
|
|||
[Conditional("DEBUG")] |
|||
private static void GuardBlockIndex(int idx) |
|||
{ |
|||
DebugGuard.MustBeLessThan(idx, Size, nameof(idx)); |
|||
DebugGuard.MustBeGreaterThanOrEqualTo(idx, 0, nameof(idx)); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override string ToString() |
|||
{ |
|||
var bld = new StringBuilder(); |
|||
bld.Append('['); |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
bld.Append(this[i]); |
|||
if (i < Size - 1) |
|||
{ |
|||
bld.Append(','); |
|||
} |
|||
} |
|||
|
|||
bld.Append(']'); |
|||
return bld.ToString(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public bool Equals(Block8x8 other) |
|||
{ |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
if (this[i] != other[i]) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (ReferenceEquals(null, obj)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return obj is Block8x8 && this.Equals((Block8x8)obj); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override int GetHashCode() |
|||
{ |
|||
return (this[0] * 31) + this[1]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculate the total sum of absoulute differences of elements in 'a' and 'b'.
|
|||
/// </summary>
|
|||
public static long TotalDifference(ref Block8x8 a, ref Block8x8 b) |
|||
{ |
|||
long result = 0; |
|||
for (int i = 0; i < Size; i++) |
|||
{ |
|||
int d = a[i] - b[i]; |
|||
result += Math.Abs(d); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Various utilities for <see cref="IJpegComponent"/>.
|
|||
/// </summary>
|
|||
internal static class ComponentUtils |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a reference to the <see cref="Block8x8"/> at the given row and column index from <see cref="IJpegComponent.SpectralBlocks"/>
|
|||
/// </summary>
|
|||
public static ref Block8x8 GetBlockReference(this IJpegComponent component, int bx, int by) |
|||
{ |
|||
return ref component.SpectralBlocks[bx, by]; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Common interface to represent raw Jpeg components.
|
|||
/// </summary>
|
|||
internal interface IJpegComponent |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the component's position in the components array.
|
|||
/// </summary>
|
|||
int Index { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of blocks in this component as <see cref="Size"/>
|
|||
/// </summary>
|
|||
Size SizeInBlocks { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal and the vertical sampling factor as <see cref="Size"/>
|
|||
/// </summary>
|
|||
Size SamplingFactors { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the divisors needed to apply when calculating colors.
|
|||
/// <see>
|
|||
/// <cref>https://en.wikipedia.org/wiki/Chroma_subsampling</cref>
|
|||
/// </see>
|
|||
/// In case of 4:2:0 subsampling the values are: Luma.SubSamplingDivisors = (1,1) Chroma.SubSamplingDivisors = (2,2)
|
|||
/// </summary>
|
|||
Size SubSamplingDivisors { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the quantization table for this block.
|
|||
/// </summary>
|
|||
int QuantizationTableIndex { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Buffer2D{Block8x8}"/> storing the "raw" frequency-domain decoded + unzigged blocks.
|
|||
/// We need to apply IDCT and dequantiazition to transform them into color-space blocks.
|
|||
/// </summary>
|
|||
Buffer2D<Block8x8> SpectralBlocks { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <inheritdoc />
|
|||
/// <summary>
|
|||
/// Represents decompressed, unprocessed jpeg data with spectral space <see cref="T:SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder.IJpegComponent" />-s.
|
|||
/// </summary>
|
|||
internal interface IRawJpegData : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the image size in pixels.
|
|||
/// </summary>
|
|||
Size ImageSizeInPixels { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of coponents.
|
|||
/// </summary>
|
|||
int ComponentCount { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color space
|
|||
/// </summary>
|
|||
JpegColorSpace ColorSpace { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the components.
|
|||
/// </summary>
|
|||
IEnumerable<IJpegComponent> Components { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the quantization tables, in zigzag order.
|
|||
/// </summary>
|
|||
Block8x8F[] QuantizationTables { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
internal abstract partial class JpegColorConverter |
|||
{ |
|||
private class FromCmyk : JpegColorConverter |
|||
{ |
|||
public FromCmyk() |
|||
: base(JpegColorSpace.Cmyk) |
|||
{ |
|||
} |
|||
|
|||
public override void ConvertToRGBA(ComponentValues values, Span<Vector4> result) |
|||
{ |
|||
// TODO: We can optimize a lot here with Vector<float> and SRCS.Unsafe()!
|
|||
ReadOnlySpan<float> cVals = values.Component0; |
|||
ReadOnlySpan<float> mVals = values.Component1; |
|||
ReadOnlySpan<float> yVals = values.Component2; |
|||
ReadOnlySpan<float> kVals = values.Component3; |
|||
|
|||
var v = new Vector4(0, 0, 0, 1F); |
|||
|
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < result.Length; i++) |
|||
{ |
|||
float c = cVals[i]; |
|||
float m = mVals[i]; |
|||
float y = yVals[i]; |
|||
float k = kVals[i] / 255F; |
|||
|
|||
v.X = c * k; |
|||
v.Y = m * k; |
|||
v.Z = y * k; |
|||
v.W = 1F; |
|||
|
|||
v *= scale; |
|||
|
|||
result[i] = v; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
internal abstract partial class JpegColorConverter |
|||
{ |
|||
private class FromGrayScale : JpegColorConverter |
|||
{ |
|||
public FromGrayScale() |
|||
: base(JpegColorSpace.GrayScale) |
|||
{ |
|||
} |
|||
|
|||
public override void ConvertToRGBA(ComponentValues values, Span<Vector4> result) |
|||
{ |
|||
// TODO: We can optimize a lot here with Vector<float> and SRCS.Unsafe()!
|
|||
ReadOnlySpan<float> yVals = values.Component0; |
|||
|
|||
var v = new Vector4(0, 0, 0, 1); |
|||
|
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < result.Length; i++) |
|||
{ |
|||
float y = yVals[i]; |
|||
|
|||
v.X = y; |
|||
v.Y = y; |
|||
v.Z = y; |
|||
|
|||
v *= scale; |
|||
|
|||
result[i] = v; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
internal abstract partial class JpegColorConverter |
|||
{ |
|||
private class FromRgb : JpegColorConverter |
|||
{ |
|||
public FromRgb() |
|||
: base(JpegColorSpace.RGB) |
|||
{ |
|||
} |
|||
|
|||
public override void ConvertToRGBA(ComponentValues values, Span<Vector4> result) |
|||
{ |
|||
// TODO: We can optimize a lot here with Vector<float> and SRCS.Unsafe()!
|
|||
ReadOnlySpan<float> rVals = values.Component0; |
|||
ReadOnlySpan<float> gVals = values.Component1; |
|||
ReadOnlySpan<float> bVals = values.Component2; |
|||
|
|||
var v = new Vector4(0, 0, 0, 1); |
|||
|
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < result.Length; i++) |
|||
{ |
|||
float r = rVals[i]; |
|||
float g = gVals[i]; |
|||
float b = bVals[i]; |
|||
|
|||
v.X = r; |
|||
v.Y = g; |
|||
v.Z = b; |
|||
|
|||
v *= scale; |
|||
|
|||
result[i] = v; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
internal abstract partial class JpegColorConverter |
|||
{ |
|||
private class FromYCbCr : JpegColorConverter |
|||
{ |
|||
public FromYCbCr() |
|||
: base(JpegColorSpace.YCbCr) |
|||
{ |
|||
} |
|||
|
|||
public override void ConvertToRGBA(ComponentValues values, Span<Vector4> result) |
|||
{ |
|||
// TODO: We can optimize a lot here with Vector<float> and SRCS.Unsafe()!
|
|||
ReadOnlySpan<float> yVals = values.Component0; |
|||
ReadOnlySpan<float> cbVals = values.Component1; |
|||
ReadOnlySpan<float> crVals = values.Component2; |
|||
|
|||
var v = new Vector4(0, 0, 0, 1); |
|||
|
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < result.Length; i++) |
|||
{ |
|||
float y = yVals[i]; |
|||
float cb = cbVals[i] - 128F; |
|||
float cr = crVals[i] - 128F; |
|||
|
|||
v.X = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero); |
|||
v.Y = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero); |
|||
v.Z = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero); |
|||
|
|||
v *= scale; |
|||
|
|||
result[i] = v; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
internal abstract partial class JpegColorConverter |
|||
{ |
|||
private class FromYccK : JpegColorConverter |
|||
{ |
|||
public FromYccK() |
|||
: base(JpegColorSpace.Ycck) |
|||
{ |
|||
} |
|||
|
|||
public override void ConvertToRGBA(ComponentValues values, Span<Vector4> result) |
|||
{ |
|||
// TODO: We can optimize a lot here with Vector<float> and SRCS.Unsafe()!
|
|||
ReadOnlySpan<float> yVals = values.Component0; |
|||
ReadOnlySpan<float> cbVals = values.Component1; |
|||
ReadOnlySpan<float> crVals = values.Component2; |
|||
ReadOnlySpan<float> kVals = values.Component3; |
|||
|
|||
var v = new Vector4(0, 0, 0, 1F); |
|||
|
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < result.Length; i++) |
|||
{ |
|||
float y = yVals[i]; |
|||
float cb = cbVals[i] - 128F; |
|||
float cr = crVals[i] - 128F; |
|||
float k = kVals[i] / 255F; |
|||
|
|||
v.X = (255F - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; |
|||
v.Y = (255F - MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * k; |
|||
v.Z = (255F - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; |
|||
v.W = 1F; |
|||
|
|||
v *= scale; |
|||
|
|||
result[i] = v; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates the conversion of Jpeg channels to RGBA values packed in <see cref="Vector4"/> buffer.
|
|||
/// </summary>
|
|||
internal abstract partial class JpegColorConverter |
|||
{ |
|||
/// <summary>
|
|||
/// The avalilable converters
|
|||
/// </summary>
|
|||
private static readonly JpegColorConverter[] Converters = { new FromYCbCr(), new FromYccK(), new FromCmyk(), new FromGrayScale(), new FromRgb() }; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="JpegColorConverter"/> class.
|
|||
/// </summary>
|
|||
protected JpegColorConverter(JpegColorSpace colorSpace) |
|||
{ |
|||
this.ColorSpace = colorSpace; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="JpegColorSpace"/> of this converter.
|
|||
/// </summary>
|
|||
public JpegColorSpace ColorSpace { get; } |
|||
|
|||
/// <summary>
|
|||
/// Returns the <see cref="JpegColorConverter"/> corresponding to the given <see cref="JpegColorSpace"/>
|
|||
/// </summary>
|
|||
public static JpegColorConverter GetConverter(JpegColorSpace colorSpace) |
|||
{ |
|||
JpegColorConverter converter = Converters.FirstOrDefault(c => c.ColorSpace == colorSpace); |
|||
if (converter == null) |
|||
{ |
|||
throw new Exception($"Could not find any converter for JpegColorSpace {colorSpace}!"); |
|||
} |
|||
|
|||
return converter; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// He implementation of the conversion.
|
|||
/// </summary>
|
|||
/// <param name="values">The input as a stack-only <see cref="ComponentValues"/> struct</param>
|
|||
/// <param name="result">The destination buffer of <see cref="Vector4"/> values</param>
|
|||
public abstract void ConvertToRGBA(ComponentValues values, Span<Vector4> result); |
|||
|
|||
/// <summary>
|
|||
/// A stack-only struct to reference the input buffers using <see cref="ReadOnlySpan{T}"/>-s.
|
|||
/// </summary>
|
|||
public struct ComponentValues |
|||
{ |
|||
/// <summary>
|
|||
/// The component count
|
|||
/// </summary>
|
|||
public readonly int ComponentCount; |
|||
|
|||
/// <summary>
|
|||
/// The component 0 (eg. Y)
|
|||
/// </summary>
|
|||
public readonly ReadOnlySpan<float> Component0; |
|||
|
|||
/// <summary>
|
|||
/// The component 1 (eg. Cb)
|
|||
/// </summary>
|
|||
public readonly ReadOnlySpan<float> Component1; |
|||
|
|||
/// <summary>
|
|||
/// The component 2 (eg. Cr)
|
|||
/// </summary>
|
|||
public readonly ReadOnlySpan<float> Component2; |
|||
|
|||
/// <summary>
|
|||
/// The component 4
|
|||
/// </summary>
|
|||
public readonly ReadOnlySpan<float> Component3; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ComponentValues"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="componentBuffers">The 1-4 sized list of component buffers.</param>
|
|||
/// <param name="row">The row to convert</param>
|
|||
public ComponentValues(IReadOnlyList<IBuffer2D<float>> componentBuffers, int row) |
|||
{ |
|||
this.ComponentCount = componentBuffers.Count; |
|||
|
|||
this.Component0 = componentBuffers[0].GetRowSpan(row); |
|||
this.Component1 = Span<float>.Empty; |
|||
this.Component2 = Span<float>.Empty; |
|||
this.Component3 = Span<float>.Empty; |
|||
|
|||
if (this.ComponentCount > 1) |
|||
{ |
|||
this.Component1 = componentBuffers[1].GetRowSpan(row); |
|||
if (this.ComponentCount > 2) |
|||
{ |
|||
this.Component2 = componentBuffers[2].GetRowSpan(row); |
|||
if (this.ComponentCount > 3) |
|||
{ |
|||
this.Component3 = componentBuffers[3].GetRowSpan(row); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Identifies the colorspace of a Jpeg image
|
|||
/// </summary>
|
|||
internal enum JpegColorSpace |
|||
{ |
|||
Undefined = 0, |
|||
|
|||
GrayScale, |
|||
|
|||
Ycck, |
|||
|
|||
Cmyk, |
|||
|
|||
RGB, |
|||
|
|||
YCbCr |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
using System; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates postprocessing data for one component for <see cref="JpegImagePostProcessor"/>.
|
|||
/// </summary>
|
|||
internal class JpegComponentPostProcessor : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Points to the current row in <see cref="Component"/>.
|
|||
/// </summary>
|
|||
private int currentComponentRowInBlocks; |
|||
|
|||
/// <summary>
|
|||
/// The size of the area in <see cref="ColorBuffer"/> corrsponding to one 8x8 Jpeg block
|
|||
/// </summary>
|
|||
private readonly Size blockAreaSize; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="JpegComponentPostProcessor"/> class.
|
|||
/// </summary>
|
|||
public JpegComponentPostProcessor(JpegImagePostProcessor imagePostProcessor, IJpegComponent component) |
|||
{ |
|||
this.Component = component; |
|||
this.ImagePostProcessor = imagePostProcessor; |
|||
this.ColorBuffer = new Buffer2D<float>(imagePostProcessor.PostProcessorBufferSize); |
|||
|
|||
this.BlockRowsPerStep = JpegImagePostProcessor.BlockRowsPerStep / this.Component.SubSamplingDivisors.Height; |
|||
this.blockAreaSize = this.Component.SubSamplingDivisors * 8; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="JpegImagePostProcessor"/>
|
|||
/// </summary>
|
|||
public JpegImagePostProcessor ImagePostProcessor { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Component"/>
|
|||
/// </summary>
|
|||
public IJpegComponent Component { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the temporal working buffer of color values.
|
|||
/// </summary>
|
|||
public Buffer2D<float> ColorBuffer { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets <see cref="IJpegComponent.SizeInBlocks"/>
|
|||
/// </summary>
|
|||
public Size SizeInBlocks => this.Component.SizeInBlocks; |
|||
|
|||
/// <summary>
|
|||
/// Gets the maximal number of block rows being processed in one step.
|
|||
/// </summary>
|
|||
public int BlockRowsPerStep { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
this.ColorBuffer.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invoke <see cref="JpegBlockPostProcessor"/> for <see cref="BlockRowsPerStep"/> block rows, copy the result into <see cref="ColorBuffer"/>.
|
|||
/// </summary>
|
|||
public unsafe void CopyBlocksToColorBuffer() |
|||
{ |
|||
var blockPp = default(JpegBlockPostProcessor); |
|||
JpegBlockPostProcessor.Init(&blockPp); |
|||
|
|||
for (int y = 0; y < this.BlockRowsPerStep; y++) |
|||
{ |
|||
int yBlock = this.currentComponentRowInBlocks + y; |
|||
|
|||
if (yBlock >= this.SizeInBlocks.Height) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
int yBuffer = y * this.blockAreaSize.Height; |
|||
|
|||
for (int x = 0; x < this.SizeInBlocks.Width; x++) |
|||
{ |
|||
int xBlock = x; |
|||
int xBuffer = x * this.blockAreaSize.Width; |
|||
|
|||
ref Block8x8 block = ref this.Component.GetBlockReference(xBlock, yBlock); |
|||
|
|||
BufferArea<float> destArea = this.ColorBuffer.GetArea( |
|||
xBuffer, |
|||
yBuffer, |
|||
this.blockAreaSize.Width, |
|||
this.blockAreaSize.Height); |
|||
|
|||
blockPp.ProcessBlockColorsInto(this.ImagePostProcessor.RawJpeg, this.Component, ref block, destArea); |
|||
} |
|||
} |
|||
|
|||
this.currentComponentRowInBlocks += this.BlockRowsPerStep; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates the execution od post-processing algorithms to be applied on a <see cref="IRawJpegData"/> to produce a valid <see cref="Image{TPixel}"/>: <br/>
|
|||
/// (1) Dequantization <br/>
|
|||
/// (2) IDCT <br/>
|
|||
/// (3) Color conversion form one of the <see cref="JpegColorSpace"/>-s into a <see cref="Vector4"/> buffer of RGBA values <br/>
|
|||
/// (4) Packing <see cref="Image{TPixel}"/> pixels from the <see cref="Vector4"/> buffer. <br/>
|
|||
/// These operations are executed in <see cref="NumberOfPostProcessorSteps"/> steps.
|
|||
/// <see cref="PixelRowsPerStep"/> image rows are converted in one step,
|
|||
/// which means that size of the allocated memory is limited (does not depend on <see cref="ImageBase{TPixel}.Height"/>).
|
|||
/// </summary>
|
|||
internal class JpegImagePostProcessor : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The number of block rows to be processed in one Step.
|
|||
/// </summary>
|
|||
public const int BlockRowsPerStep = 4; |
|||
|
|||
/// <summary>
|
|||
/// The number of image pixel rows to be processed in one step.
|
|||
/// </summary>
|
|||
public const int PixelRowsPerStep = 4 * 8; |
|||
|
|||
/// <summary>
|
|||
/// Temporal buffer to store a row of colors.
|
|||
/// </summary>
|
|||
private readonly Buffer<Vector4> rgbaBuffer; |
|||
|
|||
/// <summary>
|
|||
/// The <see cref="JpegColorConverter"/> corresponding to the current <see cref="JpegColorSpace"/> determined by <see cref="IRawJpegData.ColorSpace"/>.
|
|||
/// </summary>
|
|||
private JpegColorConverter colorConverter; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="JpegImagePostProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="rawJpeg">The <see cref="IRawJpegData"/> representing the uncompressed spectral Jpeg data</param>
|
|||
public JpegImagePostProcessor(IRawJpegData rawJpeg) |
|||
{ |
|||
this.RawJpeg = rawJpeg; |
|||
IJpegComponent c0 = rawJpeg.Components.First(); |
|||
this.NumberOfPostProcessorSteps = c0.SizeInBlocks.Height / BlockRowsPerStep; |
|||
this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep); |
|||
|
|||
this.ComponentProcessors = rawJpeg.Components.Select(c => new JpegComponentPostProcessor(this, c)).ToArray(); |
|||
this.rgbaBuffer = new Buffer<Vector4>(rawJpeg.ImageSizeInPixels.Width); |
|||
this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="JpegComponentPostProcessor"/> instances.
|
|||
/// </summary>
|
|||
public JpegComponentPostProcessor[] ComponentProcessors { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IRawJpegData"/> to be processed.
|
|||
/// </summary>
|
|||
public IRawJpegData RawJpeg { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the total number of post processor steps deduced from the height of the image and <see cref="PixelRowsPerStep"/>.
|
|||
/// </summary>
|
|||
public int NumberOfPostProcessorSteps { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size of the temporal buffers we need to allocate into <see cref="JpegComponentPostProcessor.ColorBuffer"/>.
|
|||
/// </summary>
|
|||
public Size PostProcessorBufferSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the counter that grows by each step by <see cref="PixelRowsPerStep"/>.
|
|||
/// </summary>
|
|||
public int PixelRowCounter { get; private set; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors) |
|||
{ |
|||
cpp.Dispose(); |
|||
} |
|||
|
|||
this.rgbaBuffer.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Process all pixels into 'destination'. The image dimensions should match <see cref="RawJpeg"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type</typeparam>
|
|||
/// <param name="destination">The destination image</param>
|
|||
public void PostProcess<TPixel>(Image<TPixel> destination) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
this.PixelRowCounter = 0; |
|||
|
|||
if (this.RawJpeg.ImageSizeInPixels != destination.Size()) |
|||
{ |
|||
throw new ArgumentException("Input image is not of the size of the processed one!"); |
|||
} |
|||
|
|||
while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) |
|||
{ |
|||
this.DoPostProcessorStep(destination); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute one step rocessing <see cref="PixelRowsPerStep"/> pixel rows into 'destination'.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type</typeparam>
|
|||
/// <param name="destination">The destination image.</param>
|
|||
public void DoPostProcessorStep<TPixel>(Image<TPixel> destination) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors) |
|||
{ |
|||
cpp.CopyBlocksToColorBuffer(); |
|||
} |
|||
|
|||
this.ConvertColorsInto(destination); |
|||
|
|||
this.PixelRowCounter += PixelRowsPerStep; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convert and copy <see cref="PixelRowsPerStep"/> row of colors into 'destination' starting at row <see cref="PixelRowCounter"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type</typeparam>
|
|||
/// <param name="destination">The destination image</param>
|
|||
private void ConvertColorsInto<TPixel>(Image<TPixel> destination) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
int maxY = Math.Min(destination.Height, this.PixelRowCounter + PixelRowsPerStep); |
|||
|
|||
Buffer2D<float>[] buffers = this.ComponentProcessors.Select(cp => cp.ColorBuffer).ToArray(); |
|||
|
|||
for (int yy = this.PixelRowCounter; yy < maxY; yy++) |
|||
{ |
|||
int y = yy - this.PixelRowCounter; |
|||
|
|||
var values = new JpegColorConverter.ComponentValues(buffers, y); |
|||
this.colorConverter.ConvertToRGBA(values, this.rgbaBuffer); |
|||
|
|||
Span<TPixel> destRow = destination.GetRowSpan(yy); |
|||
|
|||
PixelOperations<TPixel>.Instance.PackFromVector4(this.rgbaBuffer, destRow, destination.Width); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using System.Numerics; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Common |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for <see cref="Size"/>
|
|||
/// </summary>
|
|||
internal static class SizeExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Multiplies 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'.
|
|||
/// TODO: Shouldn't we expose this as operator in SixLabors.Core?
|
|||
/// </summary>
|
|||
public static Size MultiplyBy(this Size a, Size b) => new Size(a.Width * b.Width, a.Height * b.Height); |
|||
|
|||
/// <summary>
|
|||
/// Divides 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'.
|
|||
/// TODO: Shouldn't we expose this as operator in SixLabors.Core?
|
|||
/// </summary>
|
|||
public static Size DivideBy(this Size a, Size b) => new Size(a.Width / b.Width, a.Height / b.Height); |
|||
|
|||
/// <summary>
|
|||
/// Divide Width and Height as real numbers and return the Ceiling.
|
|||
/// </summary>
|
|||
public static Size DivideRoundUp(this Size originalSize, int divX, int divY) |
|||
{ |
|||
var sizeVect = (Vector2)(SizeF)originalSize; |
|||
sizeVect /= new Vector2(divX, divY); |
|||
sizeVect.X = MathF.Ceiling(sizeVect.X); |
|||
sizeVect.Y = MathF.Ceiling(sizeVect.Y); |
|||
|
|||
return new Size((int)sizeVect.X, (int)sizeVect.Y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Divide Width and Height as real numbers and return the Ceiling.
|
|||
/// </summary>
|
|||
public static Size DivideRoundUp(this Size originalSize, int divisor) => |
|||
DivideRoundUp(originalSize, divisor, divisor); |
|||
|
|||
/// <summary>
|
|||
/// Divide Width and Height as real numbers and return the Ceiling.
|
|||
/// </summary>
|
|||
public static Size DivideRoundUp(this Size originalSize, Size divisor) => |
|||
DivideRoundUp(originalSize, divisor.Width, divisor.Height); |
|||
} |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using Block8x8F = SixLabors.ImageSharp.Formats.Jpeg.Common.Block8x8F; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// A structure to store unprocessed <see cref="Block8x8F"/> instances and their coordinates while scanning the image.
|
|||
/// The <see cref="Block"/> is present in a "raw" decoded frequency-domain form.
|
|||
/// We need to apply IDCT and unzigging to transform them into color-space blocks.
|
|||
/// </summary>
|
|||
internal struct DecodedBlock |
|||
{ |
|||
/// <summary>
|
|||
/// A value indicating whether the <see cref="DecodedBlock"/> instance is initialized.
|
|||
/// </summary>
|
|||
public bool Initialized; |
|||
|
|||
/// <summary>
|
|||
/// X coordinate of the current block, in units of 8x8. (The third block in the first row has (bx, by) = (2, 0))
|
|||
/// </summary>
|
|||
public 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>
|
|||
public int By; |
|||
|
|||
/// <summary>
|
|||
/// The <see cref="Block8x8F"/>
|
|||
/// </summary>
|
|||
public Block8x8F Block; |
|||
|
|||
/// <summary>
|
|||
/// Store the block data into a <see cref="DecodedBlock"/>
|
|||
/// </summary>
|
|||
/// <param name="bx">X coordinate of the block</param>
|
|||
/// <param name="by">Y coordinate of the block</param>
|
|||
/// <param name="block">The <see cref="Block8x8F"/></param>
|
|||
public void SaveBlock(int bx, int by, ref Block8x8F block) |
|||
{ |
|||
this.Bx = bx; |
|||
this.By = by; |
|||
this.Block = block; |
|||
} |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a single color component
|
|||
/// </summary>
|
|||
internal struct OldComponent |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the horizontal sampling factor.
|
|||
/// </summary>
|
|||
public int HorizontalFactor; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the vertical sampling factor.
|
|||
/// </summary>
|
|||
public int VerticalFactor; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the identifier
|
|||
/// </summary>
|
|||
public byte Identifier; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the quantization table destination selector.
|
|||
/// </summary>
|
|||
public byte Selector; |
|||
} |
|||
} |
|||
@ -1,115 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using Block8x8F = SixLabors.ImageSharp.Formats.Jpeg.Common.Block8x8F; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an area of a Jpeg subimage (channel)
|
|||
/// </summary>
|
|||
internal struct OldJpegPixelArea |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OldJpegPixelArea" /> struct from existing data.
|
|||
/// </summary>
|
|||
/// <param name="pixels">The pixel buffer</param>
|
|||
/// <param name="stride">The stride</param>
|
|||
/// <param name="offset">The offset</param>
|
|||
public OldJpegPixelArea(Buffer2D<byte> pixels, int stride, int offset) |
|||
{ |
|||
this.Stride = stride; |
|||
this.Pixels = pixels; |
|||
this.Offset = offset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OldJpegPixelArea" /> struct from existing buffer.
|
|||
/// <see cref="Stride"/> will be set to <see cref="Buffer2D{T}.Width"/> of <paramref name="pixels"/> and <see cref="Offset"/> will be set to 0.
|
|||
/// </summary>
|
|||
/// <param name="pixels">The pixel buffer</param>
|
|||
public OldJpegPixelArea(Buffer2D<byte> pixels) |
|||
: this(pixels, pixels.Width, 0) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the pixels buffer.
|
|||
/// </summary>
|
|||
public Buffer2D<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 the stride.
|
|||
/// </summary>
|
|||
public int Stride { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets 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.Array, this.Offset); |
|||
|
|||
/// <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>
|
|||
/// 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 OldJpegPixelArea GetOffsetedSubAreaForBlock(int bx, int by) |
|||
{ |
|||
int offset = this.Offset + (8 * ((by * this.Stride) + bx)); |
|||
return new OldJpegPixelArea(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.Array, this.Offset), this.Stride, temp); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,244 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder |
|||
{ |
|||
/// <inheritdoc cref="IJpegComponent" />
|
|||
/// <summary>
|
|||
/// Represents a single color component
|
|||
/// </summary>
|
|||
internal class OrigComponent : IDisposable, IJpegComponent |
|||
{ |
|||
public OrigComponent(byte identifier, int index) |
|||
{ |
|||
this.Identifier = identifier; |
|||
this.Index = index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the identifier
|
|||
/// </summary>
|
|||
public byte Identifier { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public int Index { get; } |
|||
|
|||
public Size SizeInBlocks { get; private set; } |
|||
|
|||
public Size SamplingFactors { get; private set; } |
|||
|
|||
public Size SubSamplingDivisors { get; private set; } |
|||
|
|||
public int HorizontalSamplingFactor => this.SamplingFactors.Width; |
|||
|
|||
public int VerticalSamplingFactor => this.SamplingFactors.Height; |
|||
|
|||
/// <inheritdoc />
|
|||
public int QuantizationTableIndex { get; private set; } |
|||
|
|||
/// <inheritdoc />
|
|||
/// <summary>
|
|||
/// Gets the <see cref="T:SixLabors.ImageSharp.Memory.Buffer`1" /> storing the "raw" frequency-domain decoded blocks.
|
|||
/// We need to apply IDCT, dequantiazition and unzigging to transform them into color-space blocks.
|
|||
/// This is done by <see cref="M:SixLabors.ImageSharp.Formats.Jpeg.GolangPort.OrigJpegDecoderCore.ProcessBlocksIntoJpegImageChannels" />.
|
|||
/// When <see cref="P:SixLabors.ImageSharp.Formats.Jpeg.GolangPort.OrigJpegDecoderCore.IsProgressive" /> us true, we are touching these blocks multiple times - each time we process a Scan.
|
|||
/// </summary>
|
|||
public Buffer2D<Block8x8> SpectralBlocks { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Initializes <see cref="SpectralBlocks"/>
|
|||
/// </summary>
|
|||
/// <param name="decoder">The <see cref="OrigJpegDecoderCore"/> instance</param>
|
|||
public void InitializeDerivedData(OrigJpegDecoderCore decoder) |
|||
{ |
|||
// 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.
|
|||
this.SizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(this.SamplingFactors); |
|||
|
|||
if (this.Index == 0 || this.Index == 3) |
|||
{ |
|||
this.SubSamplingDivisors = new Size(1, 1); |
|||
} |
|||
else |
|||
{ |
|||
OrigComponent c0 = decoder.Components[0]; |
|||
this.SubSamplingDivisors = c0.SamplingFactors.DivideBy(this.SamplingFactors); |
|||
} |
|||
|
|||
this.SpectralBlocks = Buffer2D<Block8x8>.CreateClean(this.SizeInBlocks); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes all component data except <see cref="SpectralBlocks"/>.
|
|||
/// </summary>
|
|||
/// <param name="decoder">The <see cref="OrigJpegDecoderCore"/> instance</param>
|
|||
public void InitializeCoreData(OrigJpegDecoderCore decoder) |
|||
{ |
|||
// Section B.2.2 states that "the value of C_i shall be different from
|
|||
// the values of C_1 through C_(i-1)".
|
|||
int i = this.Index; |
|||
|
|||
for (int j = 0; j < this.Index; j++) |
|||
{ |
|||
if (this.Identifier == decoder.Components[j].Identifier) |
|||
{ |
|||
throw new ImageFormatException("Repeated component identifier"); |
|||
} |
|||
} |
|||
|
|||
this.QuantizationTableIndex = decoder.Temp[8 + (3 * i)]; |
|||
if (this.QuantizationTableIndex > OrigJpegDecoderCore.MaxTq) |
|||
{ |
|||
throw new ImageFormatException("Bad Tq value"); |
|||
} |
|||
|
|||
byte hv = decoder.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 (decoder.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.
|
|||
Size s0 = decoder.Components[0].SamplingFactors; |
|||
|
|||
if (s0.Width % h != 0 || s0.Height % v != 0) |
|||
{ |
|||
throw new ImageFormatException("Unsupported subsampling ratio"); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case 2: |
|||
{ |
|||
// Cr.
|
|||
Size s1 = decoder.Components[1].SamplingFactors; |
|||
|
|||
if (s1.Width != h || s1.Height != 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: |
|||
Size s0 = decoder.Components[0].SamplingFactors; |
|||
|
|||
if (s0.Width != h || s0.Height != v) |
|||
{ |
|||
throw new ImageFormatException("Unsupported subsampling ratio"); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
this.SamplingFactors = new Size(h, v); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.SpectralBlocks.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,182 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an image made up of three color components (luminance, blue chroma, red chroma)
|
|||
/// </summary>
|
|||
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="OldJpegPixelArea" />.
|
|||
/// </summary>
|
|||
public Buffer2D<byte> YChannel; |
|||
|
|||
/// <summary>
|
|||
/// Gets the blue chroma components channel as <see cref="OldJpegPixelArea" />.
|
|||
/// </summary>
|
|||
public Buffer2D<byte> CbChannel; |
|||
|
|||
/// <summary>
|
|||
/// Gets an offseted <see cref="OldJpegPixelArea" /> to the Cr channel
|
|||
/// </summary>
|
|||
public Buffer2D<byte> CrChannel; |
|||
#pragma warning restore SA1401
|
|||
|
|||
/// <summary>
|
|||
/// 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) |
|||
{ |
|||
Size cSize = CalculateChrominanceSize(width, height, ratio); |
|||
|
|||
this.Ratio = ratio; |
|||
this.YStride = width; |
|||
this.CStride = cSize.Width; |
|||
|
|||
this.YChannel = Buffer2D<byte>.CreateClean(width, height); |
|||
this.CbChannel = Buffer2D<byte>.CreateClean(cSize.Width, cSize.Height); |
|||
this.CrChannel = Buffer2D<byte>.CreateClean(cSize.Width, cSize.Height); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides enumeration of the various available subsample ratios.
|
|||
/// </summary>
|
|||
public enum YCbCrSubsampleRatio |
|||
{ |
|||
/// <summary>
|
|||
/// YCbCrSubsampleRatio444
|
|||
/// </summary>
|
|||
YCbCrSubsampleRatio444, |
|||
|
|||
/// <summary>
|
|||
/// YCbCrSubsampleRatio422
|
|||
/// </summary>
|
|||
YCbCrSubsampleRatio422, |
|||
|
|||
/// <summary>
|
|||
/// YCbCrSubsampleRatio420
|
|||
/// </summary>
|
|||
YCbCrSubsampleRatio420, |
|||
|
|||
/// <summary>
|
|||
/// YCbCrSubsampleRatio440
|
|||
/// </summary>
|
|||
YCbCrSubsampleRatio440, |
|||
|
|||
/// <summary>
|
|||
/// YCbCrSubsampleRatio411
|
|||
/// </summary>
|
|||
YCbCrSubsampleRatio411, |
|||
|
|||
/// <summary>
|
|||
/// YCbCrSubsampleRatio410
|
|||
/// </summary>
|
|||
YCbCrSubsampleRatio410, |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the Y slice index delta between vertically adjacent pixels.
|
|||
/// </summary>
|
|||
public int YStride { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the red and blue chroma slice index delta between vertically adjacent pixels
|
|||
/// that map to separate chroma samples.
|
|||
/// </summary>
|
|||
public int CStride { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the subsampling ratio.
|
|||
/// </summary>
|
|||
public YCbCrSubsampleRatio Ratio { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Disposes the <see cref="YCbCrImage" /> returning rented arrays to the pools.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
this.YChannel.Dispose(); |
|||
this.CbChannel.Dispose(); |
|||
this.CrChannel.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the offset of the first chroma component at the given row
|
|||
/// </summary>
|
|||
/// <param name="y">The row number.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="int" />.
|
|||
/// </returns>
|
|||
public int GetRowCOffset(int y) |
|||
{ |
|||
switch (this.Ratio) |
|||
{ |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio422: |
|||
return y * this.CStride; |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio420: |
|||
return (y / 2) * this.CStride; |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio440: |
|||
return (y / 2) * this.CStride; |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio411: |
|||
return y * this.CStride; |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio410: |
|||
return (y / 2) * this.CStride; |
|||
default: |
|||
return y * this.CStride; |
|||
} |
|||
} |
|||
|
|||
/// <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>
|
|||
/// <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: |
|||
return new Size((width + 1) / 2, height); |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio420: |
|||
return new Size((width + 1) / 2, (height + 1) / 2); |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio440: |
|||
return new Size(width, (height + 1) / 2); |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio411: |
|||
return new Size((width + 3) / 4, height); |
|||
case YCbCrSubsampleRatio.YCbCrSubsampleRatio410: |
|||
return new Size((width + 3) / 4, (height + 1) / 2); |
|||
default: |
|||
// Default to 4:4:4 subsampling.
|
|||
return new Size(width, height); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,107 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Provides 8-bit lookup tables for converting from YCbCr to Rgb colorspace.
|
|||
/// Methods to build the tables are based on libjpeg implementation.
|
|||
/// </summary>
|
|||
internal unsafe struct YCbCrToRgbTables |
|||
{ |
|||
/// <summary>
|
|||
/// The red red-chrominance table
|
|||
/// </summary>
|
|||
public fixed int CrRTable[256]; |
|||
|
|||
/// <summary>
|
|||
/// The blue blue-chrominance table
|
|||
/// </summary>
|
|||
public fixed int CbBTable[256]; |
|||
|
|||
/// <summary>
|
|||
/// The green red-chrominance table
|
|||
/// </summary>
|
|||
public fixed int CrGTable[256]; |
|||
|
|||
/// <summary>
|
|||
/// The green blue-chrominance table
|
|||
/// </summary>
|
|||
public fixed int CbGTable[256]; |
|||
|
|||
// Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places.
|
|||
private const int ScaleBits = 16; |
|||
|
|||
private const int Half = 1 << (ScaleBits - 1); |
|||
|
|||
/// <summary>
|
|||
/// Initializes the YCbCr tables
|
|||
/// </summary>
|
|||
/// <returns>The intialized <see cref="YCbCrToRgbTables"/></returns>
|
|||
public static YCbCrToRgbTables Create() |
|||
{ |
|||
YCbCrToRgbTables tables = default(YCbCrToRgbTables); |
|||
|
|||
for (int i = 0, x = -128; i <= 255; i++, x++) |
|||
{ |
|||
// i is the actual input pixel value, in the range 0..255
|
|||
// The Cb or Cr value we are thinking of is x = i - 128
|
|||
// Cr=>R value is nearest int to 1.402 * x
|
|||
tables.CrRTable[i] = RightShift((Fix(1.402F) * x) + Half); |
|||
|
|||
// Cb=>B value is nearest int to 1.772 * x
|
|||
tables.CbBTable[i] = RightShift((Fix(1.772F) * x) + Half); |
|||
|
|||
// Cr=>G value is scaled-up -0.714136286
|
|||
tables.CrGTable[i] = (-Fix(0.714136286F)) * x; |
|||
|
|||
// Cb => G value is scaled - up - 0.344136286 * x
|
|||
// We also add in Half so that need not do it in inner loop
|
|||
tables.CbGTable[i] = ((-Fix(0.344136286F)) * x) + Half; |
|||
} |
|||
|
|||
return tables; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Optimized method to pack bytes to the image from the YCbCr color space.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="packed">The packed pixel.</param>
|
|||
/// <param name="tables">The reference to the tables instance.</param>
|
|||
/// <param name="y">The y luminance component.</param>
|
|||
/// <param name="cb">The cb chroma component.</param>
|
|||
/// <param name="cr">The cr chroma component.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void Pack<TPixel>(ref TPixel packed, YCbCrToRgbTables* tables, byte y, byte cb, byte cr) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// float r = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero);
|
|||
byte r = (byte)(y + tables->CrRTable[cr]).Clamp(0, 255); |
|||
|
|||
// float g = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero);
|
|||
// The values for the G calculation are left scaled up, since we must add them together before rounding.
|
|||
byte g = (byte)(y + RightShift(tables->CbGTable[cb] + tables->CrGTable[cr])).Clamp(0, 255); |
|||
|
|||
// float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero);
|
|||
byte b = (byte)(y + tables->CbBTable[cb]).Clamp(0, 255); |
|||
|
|||
packed.PackFromRgba32(new Rgba32(r, g, b, 255)); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int Fix(float x) |
|||
{ |
|||
return (int)((x * (1L << ScaleBits)) + 0.5F); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int RightShift(int x) |
|||
{ |
|||
return x >> ScaleBits; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,823 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
|
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder; |
|||
using SixLabors.ImageSharp.MetaData; |
|||
using SixLabors.ImageSharp.MetaData.Profiles.Exif; |
|||
using SixLabors.ImageSharp.MetaData.Profiles.Icc; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort |
|||
{ |
|||
/// <inheritdoc />
|
|||
/// <summary>
|
|||
/// Performs the jpeg decoding operation.
|
|||
/// </summary>
|
|||
internal sealed unsafe class OrigJpegDecoderCore : IRawJpegData |
|||
{ |
|||
/// <summary>
|
|||
/// The maximum number of color components
|
|||
/// </summary>
|
|||
public const int MaxComponents = 4; |
|||
|
|||
/// <summary>
|
|||
/// The maximum number of quantization tables
|
|||
/// </summary>
|
|||
public const int MaxTq = 3; |
|||
|
|||
// Complex value type field + mutable + available to other classes = the field MUST NOT be private :P
|
|||
#pragma warning disable SA1401 // FieldsMustBePrivate
|
|||
|
|||
/// <summary>
|
|||
/// Encapsulates stream reading and processing data and operations for <see cref="OrigJpegDecoderCore"/>.
|
|||
/// It's a value type for imporved data locality, and reduced number of CALLVIRT-s
|
|||
/// </summary>
|
|||
public InputProcessor InputProcessor; |
|||
#pragma warning restore SA401
|
|||
|
|||
/// <summary>
|
|||
/// The global configuration
|
|||
/// </summary>
|
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// The App14 marker color-space
|
|||
/// </summary>
|
|||
private byte adobeTransform; |
|||
|
|||
/// <summary>
|
|||
/// Whether the image is in CMYK format with an App14 marker
|
|||
/// </summary>
|
|||
private bool adobeTransformValid; |
|||
|
|||
/// <summary>
|
|||
/// The horizontal resolution. Calculated if the image has a JFIF header.
|
|||
/// </summary>
|
|||
private short horizontalResolution; |
|||
|
|||
/// <summary>
|
|||
/// Whether the image has a JFIF header
|
|||
/// </summary>
|
|||
private bool isJfif; |
|||
|
|||
/// <summary>
|
|||
/// Whether the image has a EXIF header
|
|||
/// </summary>
|
|||
private bool isExif; |
|||
|
|||
/// <summary>
|
|||
/// The vertical resolution. Calculated if the image has a JFIF header.
|
|||
/// </summary>
|
|||
private short verticalResolution; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrigJpegDecoderCore" /> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="options">The options.</param>
|
|||
public OrigJpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) |
|||
{ |
|||
this.IgnoreMetadata = options.IgnoreMetadata; |
|||
this.configuration = configuration ?? Configuration.Default; |
|||
this.HuffmanTrees = OrigHuffmanTree.CreateHuffmanTrees(); |
|||
this.QuantizationTables = new Block8x8F[MaxTq + 1]; |
|||
this.Temp = new byte[2 * Block8x8F.Size]; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public JpegColorSpace ColorSpace { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the component array
|
|||
/// </summary>
|
|||
public OrigComponent[] Components { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the huffman trees
|
|||
/// </summary>
|
|||
public OrigHuffmanTree[] HuffmanTrees { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public Block8x8F[] QuantizationTables { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the temporary buffer used to store bytes read from the stream.
|
|||
/// TODO: Should be stack allocated, fixed sized buffer!
|
|||
/// </summary>
|
|||
public byte[] Temp { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public Size ImageSizeInPixels { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of MCU blocks in the image as <see cref="Size"/>.
|
|||
/// </summary>
|
|||
public Size ImageSizeInMCU { get; private set; } |
|||
|
|||
/// <inheritdoc />
|
|||
public int ComponentCount { get; private set; } |
|||
|
|||
IEnumerable<IJpegComponent> IRawJpegData.Components => this.Components; |
|||
|
|||
/// <summary>
|
|||
/// Gets the image height
|
|||
/// </summary>
|
|||
public int ImageHeight => this.ImageSizeInPixels.Height; |
|||
|
|||
/// <summary>
|
|||
/// Gets the image width
|
|||
/// </summary>
|
|||
public int ImageWidth => this.ImageSizeInPixels.Width; |
|||
|
|||
/// <summary>
|
|||
/// Gets the input stream.
|
|||
/// </summary>
|
|||
public Stream InputStream { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the image is interlaced (progressive)
|
|||
/// </summary>
|
|||
public bool IsProgressive { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the restart interval
|
|||
/// </summary>
|
|||
public int RestartInterval { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of MCU-s (Minimum Coded Units) in the image along the X axis
|
|||
/// </summary>
|
|||
public int MCUCountX => this.ImageSizeInMCU.Width; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of MCU-s (Minimum Coded Units) in the image along the Y axis
|
|||
/// </summary>
|
|||
public int MCUCountY => this.ImageSizeInMCU.Height; |
|||
|
|||
/// <summary>
|
|||
/// Gets the the total number of MCU-s (Minimum Coded Units) in the image.
|
|||
/// </summary>
|
|||
public int TotalMCUCount => this.MCUCountX * this.MCUCountY; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
|
|||
/// </summary>
|
|||
public bool IgnoreMetadata { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ImageMetaData"/> decoded by this decoder instance.
|
|||
/// </summary>
|
|||
public ImageMetaData MetaData { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Decodes the image from the specified <see cref="Stream"/> and sets
|
|||
/// the data to image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="stream">The stream, where the image should be.</param>
|
|||
/// <returns>The decoded image.</returns>
|
|||
public Image<TPixel> Decode<TPixel>(Stream stream) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
this.ParseStream(stream); |
|||
|
|||
return this.PostProcessIntoImage<TPixel>(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
for (int i = 0; i < this.HuffmanTrees.Length; i++) |
|||
{ |
|||
this.HuffmanTrees[i].Dispose(); |
|||
} |
|||
|
|||
if (this.Components != null) |
|||
{ |
|||
foreach (OrigComponent component in this.Components) |
|||
{ |
|||
component.Dispose(); |
|||
} |
|||
} |
|||
|
|||
this.InputProcessor.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Read metadata from stream and read the blocks in the scans into <see cref="OrigComponent.SpectralBlocks"/>.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream</param>
|
|||
/// <param name="metadataOnly">Whether to decode metadata only.</param>
|
|||
public void ParseStream(Stream stream, bool metadataOnly = false) |
|||
{ |
|||
this.MetaData = new ImageMetaData(); |
|||
this.InputStream = stream; |
|||
this.InputProcessor = new InputProcessor(stream, this.Temp); |
|||
|
|||
// Check for the Start Of Image marker.
|
|||
this.InputProcessor.ReadFull(this.Temp, 0, 2); |
|||
if (this.Temp[0] != OrigJpegConstants.Markers.XFF || this.Temp[1] != OrigJpegConstants.Markers.SOI) |
|||
{ |
|||
throw new ImageFormatException("Missing SOI marker."); |
|||
} |
|||
|
|||
// Process the remaining segments until the End Of Image marker.
|
|||
bool processBytes = true; |
|||
|
|||
// we can't currently short circute progressive images so don't try.
|
|||
while (processBytes) |
|||
{ |
|||
this.InputProcessor.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
|
|||
// jdmarker.c treats this as a warning (JWRN_EXTRANEOUS_DATA) and
|
|||
// continues to decode the stream. Even before next_marker sees
|
|||
// extraneous data, jpeg_fill_bit_buffer in jdhuff.c reads as many
|
|||
// bytes as it can, possibly past the end of a scan's data. It
|
|||
// effectively puts back any markers that it overscanned (e.g. an
|
|||
// "\xff\xd9" EOI marker), but it does not put back non-marker data,
|
|||
// and thus it can silently ignore a small number of extraneous
|
|||
// non-marker bytes before next_marker has a chance to see them (and
|
|||
// print a warning).
|
|||
// We are therefore also liberal in what we accept. Extraneous data
|
|||
// is silently ignore
|
|||
// This is similar to, but not exactly the same as, the restart
|
|||
// 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.InputProcessor.ReadByte(); |
|||
} |
|||
|
|||
byte marker = this.Temp[1]; |
|||
if (marker == 0) |
|||
{ |
|||
// Treat "\xff\x00" as extraneous data.
|
|||
continue; |
|||
} |
|||
|
|||
while (marker == 0xff) |
|||
{ |
|||
// Section B.1.1.2 says, "Any marker may optionally be preceded by any
|
|||
// number of fill bytes, which are bytes assigned code X'FF'".
|
|||
marker = this.InputProcessor.ReadByte(); |
|||
} |
|||
|
|||
// End Of Image.
|
|||
if (marker == OrigJpegConstants.Markers.EOI) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (marker >= OrigJpegConstants.Markers.RST0 && marker <= OrigJpegConstants.Markers.RST7) |
|||
{ |
|||
// Figures B.2 and B.16 of the specification suggest that restart markers should
|
|||
// only occur between Entropy Coded Segments and not after the final ECS.
|
|||
// However, some encoders may generate incorrect JPEGs with a final restart
|
|||
// marker. That restart marker will be seen here instead of inside the ProcessSOS
|
|||
// method, and is ignored as a harmless error. Restart markers have no extra data,
|
|||
// so we check for this before we read the 16-bit length of the segment.
|
|||
continue; |
|||
} |
|||
|
|||
// 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.InputProcessor.ReadFull(this.Temp, 0, 2); |
|||
int remaining = (this.Temp[0] << 8) + this.Temp[1] - 2; |
|||
if (remaining < 0) |
|||
{ |
|||
throw new ImageFormatException("Short segment length."); |
|||
} |
|||
|
|||
switch (marker) |
|||
{ |
|||
case OrigJpegConstants.Markers.SOF0: |
|||
case OrigJpegConstants.Markers.SOF1: |
|||
case OrigJpegConstants.Markers.SOF2: |
|||
this.IsProgressive = marker == OrigJpegConstants.Markers.SOF2; |
|||
this.ProcessStartOfFrameMarker(remaining); |
|||
if (metadataOnly && this.isJfif) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
break; |
|||
case OrigJpegConstants.Markers.DHT: |
|||
if (metadataOnly) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
} |
|||
else |
|||
{ |
|||
this.ProcessDefineHuffmanTablesMarker(remaining); |
|||
} |
|||
|
|||
break; |
|||
case OrigJpegConstants.Markers.DQT: |
|||
if (metadataOnly) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
} |
|||
else |
|||
{ |
|||
this.ProcessDqt(remaining); |
|||
} |
|||
|
|||
break; |
|||
case OrigJpegConstants.Markers.SOS: |
|||
if (metadataOnly) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// when this is a progressive image this gets called a number of times
|
|||
// need to know how many times this should be called in total.
|
|||
this.ProcessStartOfScan(remaining); |
|||
if (this.InputProcessor.ReachedEOF || !this.IsProgressive) |
|||
{ |
|||
// if unexpeced EOF reached or this is not a progressive image we can stop processing bytes as we now have the image data.
|
|||
processBytes = false; |
|||
} |
|||
|
|||
break; |
|||
case OrigJpegConstants.Markers.DRI: |
|||
if (metadataOnly) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
} |
|||
else |
|||
{ |
|||
this.ProcessDefineRestartIntervalMarker(remaining); |
|||
} |
|||
|
|||
break; |
|||
case OrigJpegConstants.Markers.APP0: |
|||
this.ProcessApplicationHeader(remaining); |
|||
break; |
|||
case OrigJpegConstants.Markers.APP1: |
|||
this.ProcessApp1Marker(remaining); |
|||
break; |
|||
case OrigJpegConstants.Markers.APP2: |
|||
this.ProcessApp2Marker(remaining); |
|||
break; |
|||
case OrigJpegConstants.Markers.APP14: |
|||
this.ProcessApp14Marker(remaining); |
|||
break; |
|||
default: |
|||
if ((marker >= OrigJpegConstants.Markers.APP0 && marker <= OrigJpegConstants.Markers.APP15) |
|||
|| marker == OrigJpegConstants.Markers.COM) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
} |
|||
else if (marker < OrigJpegConstants.Markers.SOF0) |
|||
{ |
|||
// See Table B.1 "Marker code assignments".
|
|||
throw new ImageFormatException("Unknown marker"); |
|||
} |
|||
else |
|||
{ |
|||
throw new ImageFormatException("Unknown marker"); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
this.InitDerivedMetaDataProperties(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the SOS (Start of scan marker).
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
/// <exception cref="ImageFormatException">
|
|||
/// Missing SOF Marker
|
|||
/// SOS has wrong length
|
|||
/// </exception>
|
|||
private void ProcessStartOfScan(int remaining) |
|||
{ |
|||
var scan = default(OrigJpegScanDecoder); |
|||
OrigJpegScanDecoder.InitStreamReading(&scan, this, remaining); |
|||
this.InputProcessor.Bits = default(Bits); |
|||
scan.DecodeBlocks(this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Assigns derived metadata properties to <see cref="MetaData"/>, eg. horizontal and vertical resolution if it has a JFIF header.
|
|||
/// </summary>
|
|||
private void InitDerivedMetaDataProperties() |
|||
{ |
|||
if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) |
|||
{ |
|||
this.MetaData.HorizontalResolution = this.horizontalResolution; |
|||
this.MetaData.VerticalResolution = this.verticalResolution; |
|||
} |
|||
else if (this.isExif) |
|||
{ |
|||
ExifValue horizontal = this.MetaData.ExifProfile.GetValue(ExifTag.XResolution); |
|||
ExifValue vertical = this.MetaData.ExifProfile.GetValue(ExifTag.YResolution); |
|||
double horizontalValue = horizontal != null ? ((Rational)horizontal.Value).ToDouble() : 0; |
|||
double verticalValue = vertical != null ? ((Rational)vertical.Value).ToDouble() : 0; |
|||
|
|||
if (horizontalValue > 0 && verticalValue > 0) |
|||
{ |
|||
this.MetaData.HorizontalResolution = horizontalValue; |
|||
this.MetaData.VerticalResolution = verticalValue; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a value indicating whether the image in an RGB image.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// The <see cref="bool" />.
|
|||
/// </returns>
|
|||
private bool IsRGB() |
|||
{ |
|||
if (this.isJfif) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (this.adobeTransformValid && this.adobeTransform == OrigJpegConstants.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.Components[0].Identifier == 'R' && this.Components[1].Identifier == 'G' |
|||
&& this.Components[2].Identifier == 'B'; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining number of bytes in the stream.</param>
|
|||
private void ProcessApp14Marker(int remaining) |
|||
{ |
|||
if (remaining < 12) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
return; |
|||
} |
|||
|
|||
this.InputProcessor.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.InputProcessor.Skip(remaining); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the App1 marker retrieving any stored metadata
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
private void ProcessApp1Marker(int remaining) |
|||
{ |
|||
if (remaining < 6 || this.IgnoreMetadata) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
return; |
|||
} |
|||
|
|||
byte[] profile = new byte[remaining]; |
|||
this.InputProcessor.ReadFull(profile, 0, remaining); |
|||
|
|||
if (profile[0] == 'E' && profile[1] == 'x' && profile[2] == 'i' && profile[3] == 'f' && profile[4] == '\0' |
|||
&& profile[5] == '\0') |
|||
{ |
|||
this.isExif = true; |
|||
this.MetaData.ExifProfile = new ExifProfile(profile); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the App2 marker retrieving any stored ICC profile information
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
private void ProcessApp2Marker(int remaining) |
|||
{ |
|||
// Length is 14 though we only need to check 12.
|
|||
const int Icclength = 14; |
|||
if (remaining < Icclength || this.IgnoreMetadata) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
return; |
|||
} |
|||
|
|||
byte[] identifier = new byte[Icclength]; |
|||
this.InputProcessor.ReadFull(identifier, 0, Icclength); |
|||
remaining -= Icclength; // we have read it by this point
|
|||
|
|||
if (identifier[0] == 'I' && identifier[1] == 'C' && identifier[2] == 'C' && identifier[3] == '_' |
|||
&& identifier[4] == 'P' && identifier[5] == 'R' && identifier[6] == 'O' && identifier[7] == 'F' |
|||
&& identifier[8] == 'I' && identifier[9] == 'L' && identifier[10] == 'E' && identifier[11] == '\0') |
|||
{ |
|||
byte[] profile = new byte[remaining]; |
|||
this.InputProcessor.ReadFull(profile, 0, remaining); |
|||
|
|||
if (this.MetaData.IccProfile == null) |
|||
{ |
|||
this.MetaData.IccProfile = new IccProfile(profile); |
|||
} |
|||
else |
|||
{ |
|||
this.MetaData.IccProfile.Extend(profile); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// not an ICC profile we can handle read the remaining so we can carry on and ignore this.
|
|||
this.InputProcessor.Skip(remaining); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the application header containing the JFIF identifier plus extra data.
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
private void ProcessApplicationHeader(int remaining) |
|||
{ |
|||
if (remaining < 5) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
return; |
|||
} |
|||
|
|||
this.InputProcessor.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[8] << 8)); |
|||
this.verticalResolution = (short)(this.Temp[11] + (this.Temp[10] << 8)); |
|||
} |
|||
|
|||
if (remaining > 0) |
|||
{ |
|||
this.InputProcessor.Skip(remaining); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes a Define Huffman Table marker, and initializes a huffman
|
|||
/// struct from its contents. Specified in section B.2.4.2.
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
private void ProcessDefineHuffmanTablesMarker(int remaining) |
|||
{ |
|||
while (remaining > 0) |
|||
{ |
|||
if (remaining < 17) |
|||
{ |
|||
throw new ImageFormatException("DHT has wrong length"); |
|||
} |
|||
|
|||
this.InputProcessor.ReadFull(this.Temp, 0, 17); |
|||
|
|||
int tc = this.Temp[0] >> 4; |
|||
if (tc > OrigHuffmanTree.MaxTc) |
|||
{ |
|||
throw new ImageFormatException("Bad Tc value"); |
|||
} |
|||
|
|||
int th = this.Temp[0] & 0x0f; |
|||
if (th > OrigHuffmanTree.MaxTh || (!this.IsProgressive && (th > 1))) |
|||
{ |
|||
throw new ImageFormatException("Bad Th value"); |
|||
} |
|||
|
|||
int huffTreeIndex = (tc * OrigHuffmanTree.ThRowSize) + th; |
|||
this.HuffmanTrees[huffTreeIndex].ProcessDefineHuffmanTablesMarkerLoop( |
|||
ref this.InputProcessor, |
|||
this.Temp, |
|||
ref remaining); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in
|
|||
/// macroblocks
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
private void ProcessDefineRestartIntervalMarker(int remaining) |
|||
{ |
|||
if (remaining != 2) |
|||
{ |
|||
throw new ImageFormatException("DRI has wrong length"); |
|||
} |
|||
|
|||
this.InputProcessor.ReadFull(this.Temp, 0, remaining); |
|||
this.RestartInterval = (this.Temp[0] << 8) + this.Temp[1]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1.
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
/// <exception cref="ImageFormatException">
|
|||
/// Thrown if the tables do not match the header
|
|||
/// </exception>
|
|||
private void ProcessDqt(int remaining) |
|||
{ |
|||
while (remaining > 0) |
|||
{ |
|||
bool done = false; |
|||
|
|||
remaining--; |
|||
byte x = this.InputProcessor.ReadByte(); |
|||
int tq = x & 0x0F; |
|||
if (tq > MaxTq) |
|||
{ |
|||
throw new ImageFormatException("Bad Tq value"); |
|||
} |
|||
|
|||
switch (x >> 4) |
|||
{ |
|||
case 0: |
|||
if (remaining < Block8x8F.Size) |
|||
{ |
|||
done = true; |
|||
break; |
|||
} |
|||
|
|||
remaining -= Block8x8F.Size; |
|||
this.InputProcessor.ReadFull(this.Temp, 0, Block8x8F.Size); |
|||
|
|||
for (int i = 0; i < Block8x8F.Size; i++) |
|||
{ |
|||
this.QuantizationTables[tq][i] = this.Temp[i]; |
|||
} |
|||
|
|||
break; |
|||
case 1: |
|||
if (remaining < 2 * Block8x8F.Size) |
|||
{ |
|||
done = true; |
|||
break; |
|||
} |
|||
|
|||
remaining -= 2 * Block8x8F.Size; |
|||
this.InputProcessor.ReadFull(this.Temp, 0, 2 * Block8x8F.Size); |
|||
|
|||
for (int i = 0; i < Block8x8F.Size; 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"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes the Start of Frame marker. Specified in section B.2.2.
|
|||
/// </summary>
|
|||
/// <param name="remaining">The remaining bytes in the segment block.</param>
|
|||
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.InputProcessor.ReadFull(this.Temp, 0, remaining); |
|||
|
|||
// We only support 8-bit precision.
|
|||
if (this.Temp[0] != 8) |
|||
{ |
|||
throw new ImageFormatException("Only 8-Bit precision supported."); |
|||
} |
|||
|
|||
int height = (this.Temp[1] << 8) + this.Temp[2]; |
|||
int width = (this.Temp[3] << 8) + this.Temp[4]; |
|||
|
|||
this.ImageSizeInPixels = new Size(width, height); |
|||
|
|||
if (this.Temp[5] != this.ComponentCount) |
|||
{ |
|||
throw new ImageFormatException("SOF has wrong length"); |
|||
} |
|||
|
|||
this.Components = new OrigComponent[this.ComponentCount]; |
|||
|
|||
for (int i = 0; i < this.ComponentCount; i++) |
|||
{ |
|||
byte componentIdentifier = this.Temp[6 + (3 * i)]; |
|||
var component = new OrigComponent(componentIdentifier, i); |
|||
component.InitializeCoreData(this); |
|||
this.Components[i] = component; |
|||
} |
|||
|
|||
int h0 = this.Components[0].HorizontalSamplingFactor; |
|||
int v0 = this.Components[0].VerticalSamplingFactor; |
|||
|
|||
this.ImageSizeInMCU = this.ImageSizeInPixels.DivideRoundUp(8 * h0, 8 * v0); |
|||
|
|||
foreach (OrigComponent component in this.Components) |
|||
{ |
|||
component.InitializeDerivedData(this); |
|||
} |
|||
|
|||
this.ColorSpace = this.DeduceJpegColorSpace(); |
|||
} |
|||
|
|||
private JpegColorSpace DeduceJpegColorSpace() |
|||
{ |
|||
switch (this.ComponentCount) |
|||
{ |
|||
case 1: return JpegColorSpace.GrayScale; |
|||
case 3: return this.IsRGB() ? JpegColorSpace.RGB : JpegColorSpace.YCbCr; |
|||
case 4: |
|||
|
|||
if (!this.adobeTransformValid) |
|||
{ |
|||
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
|
|||
// See https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
|||
// TODO: YCbCrA?
|
|||
if (this.adobeTransform == OrigJpegConstants.Adobe.ColorTransformYcck) |
|||
{ |
|||
return JpegColorSpace.Ycck; |
|||
} |
|||
else if (this.adobeTransform == OrigJpegConstants.Adobe.ColorTransformUnknown) |
|||
{ |
|||
// Assume CMYK
|
|||
return JpegColorSpace.Cmyk; |
|||
} |
|||
|
|||
goto default; |
|||
|
|||
default: |
|||
throw new ImageFormatException("JpegDecoder only supports RGB, CMYK and Grayscale color spaces."); |
|||
} |
|||
} |
|||
|
|||
private Image<TPixel> PostProcessIntoImage<TPixel>() |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (var postProcessor = new JpegImagePostProcessor(this)) |
|||
{ |
|||
var image = new Image<TPixel>(this.configuration, this.ImageWidth, this.ImageHeight, this.MetaData); |
|||
postProcessor.PostProcess(image); |
|||
return image; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,98 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Like corefxlab Span, but with an AddOffset() method for efficiency.
|
|||
/// TODO: When Span will be official, consider replacing this class!
|
|||
/// </summary>
|
|||
/// <see>
|
|||
/// <cref>https://github.com/dotnet/corefxlab/blob/master/src/System.Slices/System/Span.cs</cref>
|
|||
/// </see>
|
|||
/// <typeparam name="T">The type of the data in the span</typeparam>
|
|||
internal struct MutableSpan<T> |
|||
{ |
|||
/// <summary>
|
|||
/// Data
|
|||
/// </summary>
|
|||
public T[] Data; |
|||
|
|||
/// <summary>
|
|||
/// Offset
|
|||
/// </summary>
|
|||
public int Offset; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="MutableSpan{T}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="size">The size of the span</param>
|
|||
/// <param name="offset">The offset (defaults to 0)</param>
|
|||
public MutableSpan(int size, int offset = 0) |
|||
{ |
|||
this.Data = new T[size]; |
|||
this.Offset = offset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="MutableSpan{T}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="data">The data</param>
|
|||
/// <param name="offset">The offset (defaults to 0)</param>
|
|||
public MutableSpan(T[] data, int offset = 0) |
|||
{ |
|||
this.Data = data; |
|||
this.Offset = offset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the total count of data
|
|||
/// </summary>
|
|||
public int TotalCount => this.Data.Length - this.Offset; |
|||
|
|||
/// <summary>
|
|||
/// Index into the data
|
|||
/// </summary>
|
|||
/// <param name="idx">The data</param>
|
|||
/// <returns>The value at the specified index</returns>
|
|||
public T this[int idx] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
return this.Data[idx + this.Offset]; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
set |
|||
{ |
|||
this.Data[idx + this.Offset] = value; |
|||
} |
|||
} |
|||
|
|||
public static implicit operator MutableSpan<T>(T[] data) => new MutableSpan<T>(data, 0); |
|||
|
|||
/// <summary>
|
|||
/// Slice the data
|
|||
/// </summary>
|
|||
/// <param name="offset">The offset</param>
|
|||
/// <returns>The new <see cref="MutableSpan{T}"/></returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public MutableSpan<T> Slice(int offset) |
|||
{ |
|||
return new MutableSpan<T>(this.Data, this.Offset + offset); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add to the offset
|
|||
/// </summary>
|
|||
/// <param name="offset">The additional offset</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public void AddOffset(int offset) |
|||
{ |
|||
this.Offset += offset; |
|||
} |
|||
} |
|||
} |
|||
@ -1,162 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// MutableSpan Extensions
|
|||
/// </summary>
|
|||
internal static class MutableSpanExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Slice <see cref="MutableSpan{T}"/>
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the data in the span</typeparam>
|
|||
/// <param name="array">The data array</param>
|
|||
/// <param name="offset">The offset</param>
|
|||
/// <returns>The new <see cref="MutableSpan{T}"/></returns>
|
|||
public static MutableSpan<T> Slice<T>(this T[] array, int offset) => new MutableSpan<T>(array, offset); |
|||
|
|||
/// <summary>
|
|||
/// Save to a Vector4
|
|||
/// </summary>
|
|||
/// <param name="data">The data</param>
|
|||
/// <param name="v">The vector to save to</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void SaveTo(this MutableSpan<float> data, ref Vector4 v) |
|||
{ |
|||
v.X = data[0]; |
|||
v.Y = data[1]; |
|||
v.Z = data[2]; |
|||
v.W = data[3]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Save to a Vector4
|
|||
/// </summary>
|
|||
/// <param name="data">The data</param>
|
|||
/// <param name="v">The vector to save to</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void SaveTo(this MutableSpan<int> data, ref Vector4 v) |
|||
{ |
|||
v.X = data[0]; |
|||
v.Y = data[1]; |
|||
v.Z = data[2]; |
|||
v.W = data[3]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Load from Vector4
|
|||
/// </summary>
|
|||
/// <param name="data">The data</param>
|
|||
/// <param name="v">The vector to load from</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void LoadFrom(this MutableSpan<float> data, ref Vector4 v) |
|||
{ |
|||
data[0] = v.X; |
|||
data[1] = v.Y; |
|||
data[2] = v.Z; |
|||
data[3] = v.W; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Load from Vector4
|
|||
/// </summary>
|
|||
/// <param name="data">The data</param>
|
|||
/// <param name="v">The vector to load from</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void LoadFrom(this MutableSpan<int> data, ref Vector4 v) |
|||
{ |
|||
data[0] = (int)v.X; |
|||
data[1] = (int)v.Y; |
|||
data[2] = (int)v.Z; |
|||
data[3] = (int)v.W; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts all int values of src to float
|
|||
/// </summary>
|
|||
/// <param name="src">Source</param>
|
|||
/// <returns>A new <see cref="MutableSpan{T}"/> with float values</returns>
|
|||
public static MutableSpan<float> ConvertToFloat32MutableSpan(this MutableSpan<int> src) |
|||
{ |
|||
MutableSpan<float> result = new MutableSpan<float>(src.TotalCount); |
|||
for (int i = 0; i < src.TotalCount; i++) |
|||
{ |
|||
result[i] = (float)src[i]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts all float values of src to int
|
|||
/// </summary>
|
|||
/// <param name="src">Source</param>
|
|||
/// <returns>A new <see cref="MutableSpan{T}"/> with float values</returns>
|
|||
public static MutableSpan<int> ConvertToInt32MutableSpan(this MutableSpan<float> src) |
|||
{ |
|||
MutableSpan<int> result = new MutableSpan<int>(src.TotalCount); |
|||
for (int i = 0; i < src.TotalCount; i++) |
|||
{ |
|||
result[i] = (int)src[i]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add a scalar to all values of src
|
|||
/// </summary>
|
|||
/// <param name="src">The source</param>
|
|||
/// <param name="scalar">The scalar value to add</param>
|
|||
/// <returns>A new instance of <see cref="MutableSpan{T}"/></returns>
|
|||
public static MutableSpan<float> AddScalarToAllValues(this MutableSpan<float> src, float scalar) |
|||
{ |
|||
MutableSpan<float> result = new MutableSpan<float>(src.TotalCount); |
|||
for (int i = 0; i < src.TotalCount; i++) |
|||
{ |
|||
result[i] = src[i] + scalar; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add a scalar to all values of src
|
|||
/// </summary>
|
|||
/// <param name="src">The source</param>
|
|||
/// <param name="scalar">The scalar value to add</param>
|
|||
/// <returns>A new instance of <see cref="MutableSpan{T}"/></returns>
|
|||
public static MutableSpan<int> AddScalarToAllValues(this MutableSpan<int> src, int scalar) |
|||
{ |
|||
MutableSpan<int> result = new MutableSpan<int>(src.TotalCount); |
|||
for (int i = 0; i < src.TotalCount; i++) |
|||
{ |
|||
result[i] = src[i] + scalar; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copy all values in src to a new <see cref="MutableSpan{T}"/> instance
|
|||
/// </summary>
|
|||
/// <typeparam name="T">Element type</typeparam>
|
|||
/// <param name="src">The source</param>
|
|||
/// <returns>A new instance of <see cref="MutableSpan{T}"/></returns>
|
|||
public static MutableSpan<T> Copy<T>(this MutableSpan<T> src) |
|||
{ |
|||
MutableSpan<T> result = new MutableSpan<T>(src.TotalCount); |
|||
for (int i = 0; i < src.TotalCount; i++) |
|||
{ |
|||
result[i] = src[i]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort |
|||
{ |
|||
/// <summary>
|
|||
/// Image decoder for generating an image out of a jpg stream.
|
|||
/// </summary>
|
|||
internal sealed class PdfJsJpegDecoder : IImageDecoder, IJpegDecoderOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
|
|||
/// </summary>
|
|||
public bool IgnoreMetadata { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
using (var decoder = new PdfJsJpegDecoderCore(configuration, this)) |
|||
{ |
|||
return decoder.Decode<TPixel>(stream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a rectangular area inside a 2D memory buffer (<see cref="Buffer2D{T}"/>).
|
|||
/// This type is kind-of 2D Span, but it can live on heap.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The element type</typeparam>
|
|||
internal struct BufferArea<T> |
|||
where T : struct |
|||
{ |
|||
/// <summary>
|
|||
/// The rectangle specifying the boundaries of the area in <see cref="DestinationBuffer"/>.
|
|||
/// </summary>
|
|||
public readonly Rectangle Rectangle; |
|||
|
|||
public BufferArea(IBuffer2D<T> destinationBuffer, Rectangle rectangle) |
|||
{ |
|||
Guard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); |
|||
Guard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); |
|||
Guard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle)); |
|||
Guard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle)); |
|||
|
|||
this.DestinationBuffer = destinationBuffer; |
|||
this.Rectangle = rectangle; |
|||
} |
|||
|
|||
public BufferArea(IBuffer2D<T> destinationBuffer) |
|||
: this(destinationBuffer, destinationBuffer.FullRectangle()) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IBuffer2D{T}"/> being pointed by this instance.
|
|||
/// </summary>
|
|||
public IBuffer2D<T> DestinationBuffer { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size of the area.
|
|||
/// </summary>
|
|||
public Size Size => this.Rectangle.Size; |
|||
|
|||
/// <summary>
|
|||
/// Gets the pixel stride which is equal to the width of <see cref="DestinationBuffer"/>.
|
|||
/// </summary>
|
|||
public int Stride => this.DestinationBuffer.Width; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value at the given index.
|
|||
/// </summary>
|
|||
/// <param name="x">The position inside a row</param>
|
|||
/// <param name="y">The row index</param>
|
|||
/// <returns>The reference to the value</returns>
|
|||
public ref T this[int x, int y] => ref this.DestinationBuffer.Span[this.GetIndexOf(x, y)]; |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the [0,0] element.
|
|||
/// </summary>
|
|||
/// <returns>The reference to the [0,0] element</returns>
|
|||
public ref T GetReferenceToOrigo() => |
|||
ref this.DestinationBuffer.Span[(this.Rectangle.Y * this.DestinationBuffer.Width) + this.Rectangle.X]; |
|||
|
|||
/// <summary>
|
|||
/// Gets a span to row 'y' inside this area.
|
|||
/// </summary>
|
|||
/// <param name="y">The row index</param>
|
|||
/// <returns>The span</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public Span<T> GetRowSpan(int y) |
|||
{ |
|||
int yy = this.GetRowIndex(y); |
|||
int xx = this.Rectangle.X; |
|||
int width = this.Rectangle.Width; |
|||
|
|||
return this.DestinationBuffer.Span.Slice(yy + xx, width); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a sub-area as <see cref="BufferArea{T}"/>. (Similar to <see cref="Span{T}.Slice(int, int)"/>.)
|
|||
/// </summary>
|
|||
/// <param name="x">The x index at the subarea origo</param>
|
|||
/// <param name="y">The y index at the subarea origo</param>
|
|||
/// <param name="width">The desired width of the subarea</param>
|
|||
/// <param name="height">The desired height of the subarea</param>
|
|||
/// <returns>The subarea</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public BufferArea<T> GetSubArea(int x, int y, int width, int height) |
|||
{ |
|||
var rectangle = new Rectangle(x, y, width, height); |
|||
return this.GetSubArea(rectangle); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a sub-area as <see cref="BufferArea{T}"/>. (Similar to <see cref="Span{T}.Slice(int, int)"/>.)
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The <see cref="Rectangle"/> specifying the boundaries of the subarea</param>
|
|||
/// <returns>The subarea</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public BufferArea<T> GetSubArea(Rectangle rectangle) |
|||
{ |
|||
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Rectangle.Width, nameof(rectangle)); |
|||
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Rectangle.Height, nameof(rectangle)); |
|||
|
|||
int x = this.Rectangle.X + rectangle.X; |
|||
int y = this.Rectangle.Y + rectangle.Y; |
|||
rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height); |
|||
return new BufferArea<T>(this.DestinationBuffer, rectangle); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetIndexOf(int x, int y) |
|||
{ |
|||
int yy = this.GetRowIndex(y); |
|||
int xx = this.Rectangle.X + x; |
|||
return yy + xx; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetRowIndex(int y) |
|||
{ |
|||
return (y + this.Rectangle.Y) * this.DestinationBuffer.Width; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Memory |
|||
{ |
|||
/// <inheritdoc />
|
|||
/// <summary>
|
|||
/// Represents a contigous memory buffer of value-type items "promising" a <see cref="Span{T}"/>
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type</typeparam>
|
|||
internal interface IBuffer<T> : IDisposable |
|||
where T : struct |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the span to the memory "promised" by this buffer
|
|||
/// </summary>
|
|||
Span<T> Span { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
|
|||
|
|||
// Uncomment this to turn unit tests into benchmarks:
|
|||
//#define BENCHMARKING
|
|||
|
|||
// ReSharper disable InconsistentNaming
|
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; |
|||
using SixLabors.Primitives; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
public partial class Block8x8FTests : JpegFixture |
|||
{ |
|||
public class CopyToBufferArea : JpegFixture |
|||
{ |
|||
public CopyToBufferArea(ITestOutputHelper output) |
|||
: base(output) |
|||
{ |
|||
} |
|||
|
|||
private static void VerifyAllZeroOutsideSubArea(Buffer2D<float> buffer, int subX, int subY, int horizontalFactor = 1, int verticalFactor = 1) |
|||
{ |
|||
for (int y = 0; y < 20; y++) |
|||
{ |
|||
for (int x = 0; x < 20; x++) |
|||
{ |
|||
if (x < subX || x >= subX + 8 * horizontalFactor || y < subY || y >= subY + 8 * verticalFactor) |
|||
{ |
|||
Assert.Equal(0, buffer[x, y]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// TODO: This test occasionally fails. Don't get the reason, BufferArea.CopyTo() is totally OK.
|
|||
[Fact(Skip = "This test occasionally fails. Don't get the reason, BufferArea.CopyTo() is totally OK.")] |
|||
public void Unscaled() |
|||
{ |
|||
Block8x8F block = CreateRandomFloatBlock(0, 100); |
|||
|
|||
using (var buffer = new Buffer2D<float>(20, 20)) |
|||
{ |
|||
BufferArea<float> area = buffer.GetArea(5, 10, 8, 8); |
|||
block.CopyTo(area); |
|||
|
|||
Assert.Equal(block[0, 0], buffer[5, 10]); |
|||
Assert.Equal(block[1, 0], buffer[6, 10]); |
|||
Assert.Equal(block[0, 1], buffer[5, 11]); |
|||
Assert.Equal(block[0, 7], buffer[5, 17]); |
|||
Assert.Equal(block[63], buffer[12, 17]); |
|||
|
|||
VerifyAllZeroOutsideSubArea(buffer, 5, 10); |
|||
} |
|||
} |
|||
|
|||
// TODO: This test occasionally fails. Don't get the reason, BufferArea.CopyTo() is totally OK.
|
|||
[Theory(Skip = "This test occasionally fails. Don't get the reason, BufferArea.CopyTo() is totally OK.")] |
|||
[InlineData(1, 1)] |
|||
[InlineData(1, 2)] |
|||
[InlineData(2, 1)] |
|||
[InlineData(2, 2)] |
|||
[InlineData(4, 2)] |
|||
[InlineData(4, 4)] |
|||
public void Scaled(int horizontalFactor, int verticalFactor) |
|||
{ |
|||
Block8x8F block = CreateRandomFloatBlock(0, 100); |
|||
|
|||
var start = new Point(50, 50); |
|||
|
|||
using (var buffer = new Buffer2D<float>(100, 100)) |
|||
{ |
|||
BufferArea<float> area = buffer.GetArea(start.X, start.Y, 8 * horizontalFactor, 8 * verticalFactor); |
|||
block.CopyTo(area, horizontalFactor, verticalFactor); |
|||
|
|||
for (int y = 0; y < 8 * verticalFactor; y++) |
|||
{ |
|||
for (int x = 0; x < 8 * horizontalFactor; x++) |
|||
{ |
|||
int yy = y / verticalFactor; |
|||
int xx = x / horizontalFactor; |
|||
|
|||
float expected = block[xx, yy]; |
|||
float actual = area[x, y]; |
|||
|
|||
Assert.Equal(expected, actual); |
|||
} |
|||
} |
|||
|
|||
VerifyAllZeroOutsideSubArea(buffer, start.X, start.Y, horizontalFactor, verticalFactor); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
// ReSharper disable InconsistentNaming
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common; |
|||
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
public class Block8x8Tests : JpegFixture |
|||
{ |
|||
public Block8x8Tests(ITestOutputHelper output) |
|||
: base(output) |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public void Construct_And_Indexer_Get() |
|||
{ |
|||
short[] data = Create8x8ShortData(); |
|||
|
|||
var block = new Block8x8(data); |
|||
|
|||
for (int i = 0; i < Block8x8.Size; i++) |
|||
{ |
|||
Assert.Equal(data[i], block[i]); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Indexer_Set() |
|||
{ |
|||
var block = default(Block8x8); |
|||
|
|||
block[17] = 17; |
|||
block[42] = 42; |
|||
|
|||
Assert.Equal(0, block[0]); |
|||
Assert.Equal(17, block[17]); |
|||
Assert.Equal(42, block[42]); |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public unsafe void Indexer_GetScalarAt_SetScalarAt() |
|||
{ |
|||
int sum = 0; |
|||
var block = default(Block8x8); |
|||
|
|||
for (int i = 0; i < Block8x8.Size; i++) |
|||
{ |
|||
Block8x8.SetScalarAt(&block, i, (short)i); |
|||
} |
|||
|
|||
sum = 0; |
|||
for (int i = 0; i < Block8x8.Size; i++) |
|||
{ |
|||
sum += Block8x8.GetScalarAt(&block, i); |
|||
} |
|||
Assert.Equal(sum, 64 * 63 / 2); |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void AsFloatBlock() |
|||
{ |
|||
short[] data = Create8x8ShortData(); |
|||
|
|||
var source = new Block8x8(data); |
|||
|
|||
Block8x8F dest = source.AsFloatBlock(); |
|||
|
|||
for (int i = 0; i < Block8x8F.Size; i++) |
|||
{ |
|||
Assert.Equal((float)data[i], dest[i]); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void ToArray() |
|||
{ |
|||
short[] data = Create8x8ShortData(); |
|||
var block = new Block8x8(data); |
|||
|
|||
short[] result = block.ToArray(); |
|||
|
|||
Assert.Equal(data, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Equality_WhenTrue() |
|||
{ |
|||
short[] data = Create8x8ShortData(); |
|||
var block1 = new Block8x8(data); |
|||
var block2 = new Block8x8(data); |
|||
|
|||
block1[0] = 42; |
|||
block2[0] = 42; |
|||
|
|||
Assert.Equal(block1, block2); |
|||
Assert.Equal(block1.GetHashCode(), block2.GetHashCode()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Equality_WhenFalse() |
|||
{ |
|||
short[] data = Create8x8ShortData(); |
|||
var block1 = new Block8x8(data); |
|||
var block2 = new Block8x8(data); |
|||
|
|||
block1[0] = 42; |
|||
block2[0] = 666; |
|||
|
|||
Assert.NotEqual(block1, block2); |
|||
} |
|||
|
|||
[Fact] |
|||
public void IndexerXY() |
|||
{ |
|||
var block = default(Block8x8); |
|||
block[8 * 3 + 5] = 42; |
|||
|
|||
short value = block[5, 3]; |
|||
|
|||
Assert.Equal(42, value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TotalDifference() |
|||
{ |
|||
short[] data = Create8x8ShortData(); |
|||
var block1 = new Block8x8(data); |
|||
var block2 = new Block8x8(data); |
|||
|
|||
block2[10] += 7; |
|||
block2[63] += 8; |
|||
|
|||
long d = Block8x8.TotalDifference(ref block1, ref block2); |
|||
|
|||
Assert.Equal(15, d); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,182 @@ |
|||
// ReSharper disable InconsistentNaming
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components; |
|||
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
public static class DCTTests |
|||
{ |
|||
public class FastFloatingPoint : JpegFixture |
|||
{ |
|||
public FastFloatingPoint(ITestOutputHelper output) |
|||
: base(output) |
|||
{ |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void iDCT2D8x4_LeftPart() |
|||
{ |
|||
float[] sourceArray = JpegFixture.Create8x8FloatData(); |
|||
float[] expectedDestArray = new float[64]; |
|||
|
|||
ReferenceImplementations.LLM_FloatingPoint_DCT.iDCT2D8x4_32f(sourceArray, expectedDestArray); |
|||
|
|||
Block8x8F source = new Block8x8F(); |
|||
source.LoadFrom(sourceArray); |
|||
|
|||
Block8x8F dest = new Block8x8F(); |
|||
|
|||
FastFloatingPointDCT.IDCT8x4_LeftPart(ref source, ref dest); |
|||
|
|||
float[] actualDestArray = new float[64]; |
|||
dest.CopyTo(actualDestArray); |
|||
|
|||
this.Print8x8Data(expectedDestArray); |
|||
this.Output.WriteLine("**************"); |
|||
this.Print8x8Data(actualDestArray); |
|||
|
|||
Assert.Equal(expectedDestArray, actualDestArray); |
|||
} |
|||
|
|||
[Fact] |
|||
public void iDCT2D8x4_RightPart() |
|||
{ |
|||
float[] sourceArray = JpegFixture.Create8x8FloatData(); |
|||
float[] expectedDestArray = new float[64]; |
|||
|
|||
ReferenceImplementations.LLM_FloatingPoint_DCT.iDCT2D8x4_32f(sourceArray.AsSpan().Slice(4), expectedDestArray.AsSpan().Slice(4)); |
|||
|
|||
Block8x8F source = new Block8x8F(); |
|||
source.LoadFrom(sourceArray); |
|||
|
|||
Block8x8F dest = new Block8x8F(); |
|||
|
|||
FastFloatingPointDCT.IDCT8x4_RightPart(ref source, ref dest); |
|||
|
|||
float[] actualDestArray = new float[64]; |
|||
dest.CopyTo(actualDestArray); |
|||
|
|||
this.Print8x8Data(expectedDestArray); |
|||
this.Output.WriteLine("**************"); |
|||
this.Print8x8Data(actualDestArray); |
|||
|
|||
Assert.Equal(expectedDestArray, actualDestArray); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
[InlineData(3)] |
|||
public void LLM_TransformIDCT_CompareToNonOptimized(int seed) |
|||
{ |
|||
float[] sourceArray = JpegFixture.Create8x8RoundedRandomFloatData(-1000, 1000, seed); |
|||
|
|||
var source = Block8x8F.Load(sourceArray); |
|||
|
|||
Block8x8F expected = ReferenceImplementations.LLM_FloatingPoint_DCT.TransformIDCT(ref source); |
|||
|
|||
var temp = default(Block8x8F); |
|||
var actual = default(Block8x8F); |
|||
FastFloatingPointDCT.TransformIDCT(ref source, ref actual, ref temp); |
|||
|
|||
this.CompareBlocks(expected, actual, 1f); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
[InlineData(3)] |
|||
public void LLM_TransformIDCT_CompareToAccurate(int seed) |
|||
{ |
|||
float[] sourceArray = JpegFixture.Create8x8RoundedRandomFloatData(-1000, 1000, seed); |
|||
|
|||
var source = Block8x8F.Load(sourceArray); |
|||
|
|||
Block8x8F expected = ReferenceImplementations.AccurateDCT.TransformIDCT(ref source); |
|||
|
|||
var temp = default(Block8x8F); |
|||
var actual = default(Block8x8F); |
|||
FastFloatingPointDCT.TransformIDCT(ref source, ref actual, ref temp); |
|||
|
|||
this.CompareBlocks(expected, actual, 1f); |
|||
} |
|||
|
|||
|
|||
[Theory] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
public void FDCT8x4_LeftPart(int seed) |
|||
{ |
|||
Span<float> src = JpegFixture.Create8x8RoundedRandomFloatData(-200, 200, seed); |
|||
Block8x8F srcBlock = new Block8x8F(); |
|||
srcBlock.LoadFrom(src); |
|||
|
|||
Block8x8F destBlock = new Block8x8F(); |
|||
|
|||
float[] expectedDest = new float[64]; |
|||
|
|||
ReferenceImplementations.LLM_FloatingPoint_DCT.fDCT2D8x4_32f(src, expectedDest); |
|||
FastFloatingPointDCT.FDCT8x4_LeftPart(ref srcBlock, ref destBlock); |
|||
|
|||
float[] actualDest = new float[64]; |
|||
destBlock.CopyTo(actualDest); |
|||
|
|||
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
public void FDCT8x4_RightPart(int seed) |
|||
{ |
|||
Span<float> src = JpegFixture.Create8x8RoundedRandomFloatData(-200, 200, seed); |
|||
Block8x8F srcBlock = new Block8x8F(); |
|||
srcBlock.LoadFrom(src); |
|||
|
|||
Block8x8F destBlock = new Block8x8F(); |
|||
|
|||
float[] expectedDest = new float[64]; |
|||
|
|||
ReferenceImplementations.LLM_FloatingPoint_DCT.fDCT2D8x4_32f(src.Slice(4), expectedDest.AsSpan().Slice(4)); |
|||
FastFloatingPointDCT.FDCT8x4_RightPart(ref srcBlock, ref destBlock); |
|||
|
|||
float[] actualDest = new float[64]; |
|||
destBlock.CopyTo(actualDest); |
|||
|
|||
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
public void TransformFDCT(int seed) |
|||
{ |
|||
Span<float> src = JpegFixture.Create8x8RoundedRandomFloatData(-200, 200, seed); |
|||
Block8x8F srcBlock = new Block8x8F(); |
|||
srcBlock.LoadFrom(src); |
|||
|
|||
Block8x8F destBlock = new Block8x8F(); |
|||
|
|||
float[] expectedDest = new float[64]; |
|||
float[] temp1 = new float[64]; |
|||
Block8x8F temp2 = new Block8x8F(); |
|||
|
|||
ReferenceImplementations.LLM_FloatingPoint_DCT.fDCT2D_llm(src, expectedDest, temp1, downscaleBy8: true); |
|||
FastFloatingPointDCT.TransformFDCT(ref srcBlock, ref destBlock, ref temp2, false); |
|||
|
|||
float[] actualDest = new float[64]; |
|||
destBlock.CopyTo(actualDest); |
|||
|
|||
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f)); |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,192 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
using SixLabors.ImageSharp.ColorSpaces; |
|||
using SixLabors.ImageSharp.ColorSpaces.Conversion; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
public class JpegColorConverterTests |
|||
{ |
|||
private const float Precision = 0.1f; |
|||
|
|||
private const int InputBufferLength = 42; |
|||
|
|||
// The result buffer could be shorter
|
|||
private const int ResultBufferLength = 40; |
|||
|
|||
private readonly Vector4[] result = new Vector4[ResultBufferLength]; |
|||
|
|||
private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter(); |
|||
|
|||
public JpegColorConverterTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
private static JpegColorConverter.ComponentValues CreateRandomValues(int componentCount, float maxVal = 255f) |
|||
{ |
|||
var rnd = new Random(42); |
|||
Buffer2D<float>[] buffers = new Buffer2D<float>[componentCount]; |
|||
for (int i = 0; i < componentCount; i++) |
|||
{ |
|||
float[] values = new float[InputBufferLength]; |
|||
|
|||
for (int j = 0; j < InputBufferLength; j++) |
|||
{ |
|||
values[j] = (float)rnd.NextDouble() * maxVal; |
|||
} |
|||
|
|||
// no need to dispose when buffer is not array owner
|
|||
buffers[i] = new Buffer2D<float>(values, values.Length, 1); |
|||
} |
|||
return new JpegColorConverter.ComponentValues(buffers, 0); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertFromYCbCr() |
|||
{ |
|||
var converter = JpegColorConverter.GetConverter(JpegColorSpace.YCbCr); |
|||
|
|||
JpegColorConverter.ComponentValues values = CreateRandomValues(3); |
|||
|
|||
converter.ConvertToRGBA(values, this.result); |
|||
|
|||
for (int i = 0; i < ResultBufferLength; i++) |
|||
{ |
|||
float y = values.Component0[i]; |
|||
float cb = values.Component1[i]; |
|||
float cr = values.Component2[i]; |
|||
var ycbcr = new YCbCr(y, cb, cr); |
|||
|
|||
Vector4 rgba = this.result[i]; |
|||
var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); |
|||
var expected = ColorSpaceConverter.ToRgb(ycbcr); |
|||
|
|||
Assert.True(actual.AlmostEquals(expected, Precision)); |
|||
Assert.Equal(1, rgba.W); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertFromCmyk() |
|||
{ |
|||
var converter = JpegColorConverter.GetConverter(JpegColorSpace.Cmyk); |
|||
|
|||
JpegColorConverter.ComponentValues values = CreateRandomValues(4); |
|||
|
|||
converter.ConvertToRGBA(values, this.result); |
|||
|
|||
var v = new Vector4(0, 0, 0, 1F); |
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < ResultBufferLength; i++) |
|||
{ |
|||
float c = values.Component0[i]; |
|||
float m = values.Component1[i]; |
|||
float y = values.Component2[i]; |
|||
float k = values.Component3[i] / 255F; |
|||
|
|||
v.X = c * k; |
|||
v.Y = m * k; |
|||
v.Z = y * k; |
|||
v.W = 1F; |
|||
|
|||
v *= scale; |
|||
|
|||
Vector4 rgba = this.result[i]; |
|||
var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); |
|||
var expected = new Rgb(v.X, v.Y, v.Z); |
|||
|
|||
Assert.True(actual.AlmostEquals(expected, Precision)); |
|||
Assert.Equal(1, rgba.W); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertFromYcck() |
|||
{ |
|||
var converter = JpegColorConverter.GetConverter(JpegColorSpace.Ycck); |
|||
|
|||
JpegColorConverter.ComponentValues values = CreateRandomValues(4); |
|||
|
|||
converter.ConvertToRGBA(values, this.result); |
|||
|
|||
var v = new Vector4(0, 0, 0, 1F); |
|||
var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); |
|||
|
|||
for (int i = 0; i < ResultBufferLength; i++) |
|||
{ |
|||
float y = values.Component0[i]; |
|||
float cb = values.Component1[i] - 128F; |
|||
float cr = values.Component2[i] - 128F; |
|||
float k = values.Component3[i] / 255F; |
|||
|
|||
v.X = (255F - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; |
|||
v.Y = (255F - MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * k; |
|||
v.Z = (255F - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; |
|||
v.W = 1F; |
|||
|
|||
v *= scale; |
|||
|
|||
Vector4 rgba = this.result[i]; |
|||
var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); |
|||
var expected = new Rgb(v.X, v.Y, v.Z); |
|||
|
|||
Assert.True(actual.AlmostEquals(expected, Precision)); |
|||
Assert.Equal(1, rgba.W); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertFromGrayScale() |
|||
{ |
|||
var converter = JpegColorConverter.GetConverter(JpegColorSpace.GrayScale); |
|||
|
|||
JpegColorConverter.ComponentValues values = CreateRandomValues(1); |
|||
|
|||
converter.ConvertToRGBA(values, this.result); |
|||
|
|||
for (int i = 0; i < ResultBufferLength; i++) |
|||
{ |
|||
float y = values.Component0[i]; |
|||
Vector4 rgba = this.result[i]; |
|||
var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); |
|||
var expected = new Rgb(y / 255F, y / 255F, y / 255F); |
|||
|
|||
Assert.True(actual.AlmostEquals(expected, Precision)); |
|||
Assert.Equal(1, rgba.W); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertFromRgb() |
|||
{ |
|||
var converter = JpegColorConverter.GetConverter(JpegColorSpace.RGB); |
|||
|
|||
JpegColorConverter.ComponentValues values = CreateRandomValues(3); |
|||
|
|||
converter.ConvertToRGBA(values, this.result); |
|||
|
|||
for (int i = 0; i < ResultBufferLength; i++) |
|||
{ |
|||
float r = values.Component0[i]; |
|||
float g = values.Component1[i]; |
|||
float b = values.Component2[i]; |
|||
Vector4 rgba = this.result[i]; |
|||
var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); |
|||
var expected = new Rgb(r / 255F, g / 255F, b / 255F); |
|||
|
|||
Assert.True(actual.AlmostEquals(expected, Precision)); |
|||
Assert.Equal(1, rgba.W); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
public class JpegImagePostProcessorTests |
|||
{ |
|||
public static string[] BaselineTestJpegs = |
|||
{ |
|||
TestImages.Jpeg.Baseline.Calliphora, |
|||
TestImages.Jpeg.Baseline.Cmyk, |
|||
TestImages.Jpeg.Baseline.Ycck, |
|||
TestImages.Jpeg.Baseline.Jpeg400, |
|||
TestImages.Jpeg.Baseline.Testorig420, |
|||
TestImages.Jpeg.Baseline.Jpeg420Small, |
|||
TestImages.Jpeg.Baseline.Jpeg444, |
|||
TestImages.Jpeg.Baseline.Bad.BadEOF, |
|||
TestImages.Jpeg.Baseline.Bad.ExifUndefType, |
|||
}; |
|||
|
|||
public static string[] ProgressiveTestJpegs = |
|||
{ |
|||
TestImages.Jpeg.Progressive.Fb, TestImages.Jpeg.Progressive.Progress, |
|||
TestImages.Jpeg.Progressive.Festzug, TestImages.Jpeg.Progressive.Bad.BadEOF |
|||
}; |
|||
|
|||
public JpegImagePostProcessorTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
private static void SaveBuffer<TPixel>(JpegComponentPostProcessor cp, TestImageProvider<TPixel> provider) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (Image<Rgba32> image = cp.ColorBuffer.ToGrayscaleImage(1f / 255f)) |
|||
{ |
|||
image.DebugSave(provider, $"-C{cp.Component.Index}-"); |
|||
} |
|||
|
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Jpeg.Baseline.Calliphora, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)] |
|||
public void DoProcessorStep<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
string imageFile = provider.SourceFileOrDescription; |
|||
using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile)) |
|||
using (var pp = new JpegImagePostProcessor(decoder)) |
|||
using (var image = new Image<Rgba32>(decoder.ImageWidth, decoder.ImageHeight)) |
|||
{ |
|||
pp.DoPostProcessorStep(image); |
|||
|
|||
JpegComponentPostProcessor[] cp = pp.ComponentProcessors; |
|||
|
|||
SaveBuffer(cp[0], provider); |
|||
SaveBuffer(cp[1], provider); |
|||
SaveBuffer(cp[2], provider); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Jpeg.Baseline.Calliphora, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)] |
|||
public void PostProcess<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
string imageFile = provider.SourceFileOrDescription; |
|||
using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile)) |
|||
using (var pp = new JpegImagePostProcessor(decoder)) |
|||
using (var image = new Image<Rgba32>(decoder.ImageWidth, decoder.ImageHeight)) |
|||
{ |
|||
pp.PostProcess(image); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
ImagingTestCaseUtility testUtil = provider.Utility; |
|||
testUtil.TestGroupName = nameof(JpegDecoderTests); |
|||
testUtil.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; |
|||
|
|||
using (Image<TPixel> referenceImage = |
|||
provider.GetReferenceOutputImage<TPixel>(appendPixelTypeToFileName: false)) |
|||
{ |
|||
ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); |
|||
|
|||
this.Output.WriteLine($"*** {imageFile} ***"); |
|||
this.Output.WriteLine($"Difference: "+ report.DifferencePercentageString); |
|||
|
|||
// ReSharper disable once PossibleInvalidOperationException
|
|||
Assert.True(report.TotalNormalizedDifference.Value < 0.005f); |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -1,103 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Text; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils; |
|||
using Xunit.Abstractions; |
|||
|
|||
// ReSharper disable InconsistentNaming
|
|||
|
|||
namespace SixLabors.ImageSharp.Tests |
|||
{ |
|||
public class JpegUtilityTestFixture : MeasureFixture |
|||
{ |
|||
public JpegUtilityTestFixture(ITestOutputHelper output) : base(output) |
|||
{ |
|||
} |
|||
|
|||
// ReSharper disable once InconsistentNaming
|
|||
public static float[] Create8x8FloatData() |
|||
{ |
|||
float[] result = new float[64]; |
|||
for (int i = 0; i < 8; i++) |
|||
{ |
|||
for (int j = 0; j < 8; j++) |
|||
{ |
|||
result[i * 8 + j] = i * 10 + j; |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
// ReSharper disable once InconsistentNaming
|
|||
public static int[] Create8x8IntData() |
|||
{ |
|||
int[] result = new int[64]; |
|||
for (int i = 0; i < 8; i++) |
|||
{ |
|||
for (int j = 0; j < 8; j++) |
|||
{ |
|||
result[i * 8 + j] = i * 10 + j; |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
// ReSharper disable once InconsistentNaming
|
|||
public static int[] Create8x8RandomIntData(int minValue, int maxValue, int seed = 42) |
|||
{ |
|||
Random rnd = new Random(seed); |
|||
int[] result = new int[64]; |
|||
for (int i = 0; i < 8; i++) |
|||
{ |
|||
for (int j = 0; j < 8; j++) |
|||
{ |
|||
result[i * 8 + j] = rnd.Next(minValue, maxValue); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
internal static MutableSpan<float> Create8x8RandomFloatData(int minValue, int maxValue, int seed = 42) |
|||
=> new MutableSpan<int>(Create8x8RandomIntData(minValue, maxValue, seed)).ConvertToFloat32MutableSpan(); |
|||
|
|||
internal void Print8x8Data<T>(MutableSpan<T> data) => this.Print8x8Data(data.Data); |
|||
|
|||
internal void Print8x8Data<T>(T[] data) |
|||
{ |
|||
StringBuilder bld = new StringBuilder(); |
|||
for (int i = 0; i < 8; i++) |
|||
{ |
|||
for (int j = 0; j < 8; j++) |
|||
{ |
|||
bld.Append($"{data[i * 8 + j],3} "); |
|||
} |
|||
bld.AppendLine(); |
|||
} |
|||
|
|||
this.Output.WriteLine(bld.ToString()); |
|||
} |
|||
|
|||
internal void PrintLinearData<T>(T[] data) => this.PrintLinearData(new MutableSpan<T>(data), data.Length); |
|||
|
|||
internal void PrintLinearData<T>(MutableSpan<T> data, int count = -1) |
|||
{ |
|||
if (count < 0) count = data.TotalCount; |
|||
|
|||
StringBuilder bld = new StringBuilder(); |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
bld.Append($"{data[i],3} "); |
|||
} |
|||
this.Output.WriteLine(bld.ToString()); |
|||
} |
|||
|
|||
protected void Print(string msg) |
|||
{ |
|||
Debug.WriteLine(msg); |
|||
this.Output.WriteLine(msg); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue