diff --git a/src/ImageSharp/Formats/Jpg/Components/Bits.cs b/src/ImageSharp/Formats/Jpg/Components/Bits.cs index 68278e69bf..975c92aee0 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Bits.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Bits.cs @@ -5,27 +5,92 @@ namespace ImageSharp.Formats { + using System.Runtime.CompilerServices; + /// /// Holds the unprocessed bits that have been taken from the byte-stream. /// The n least significant bits of a form the unread bits, to be read in MSB to /// LSB order. /// - internal class Bits + internal struct Bits { /// /// Gets or sets the accumulator. /// - public uint Accumulator { get; set; } + public uint Accumulator; /// /// Gets or sets the mask. /// 0, with mask==0 when unreadbits==0.]]> /// - public uint Mask { get; set; } + public uint Mask; /// /// Gets or sets the number of unread bits in the accumulator. /// - public int UnreadBits { get; set; } + public int UnreadBits; + + /// + /// Reads bytes from the byte buffer to ensure that bits.UnreadBits is at + /// least n. For best performance (avoiding function calls inside hot loops), + /// the caller is the one responsible for first checking that bits.UnreadBits < n. + /// + /// The number of bits to ensure. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal JpegDecoderCore.ErrorCodes EnsureNBits(int n, JpegDecoderCore decoder) + { + while (true) + { + JpegDecoderCore.ErrorCodes errorCode; + + byte c = decoder.bytes.ReadByteStuffedByte(decoder.inputStream, out errorCode); + + if (errorCode != JpegDecoderCore.ErrorCodes.NoError) + { + return errorCode; + } + + this.Accumulator = (this.Accumulator << 8) | c; + this.UnreadBits += 8; + if (this.Mask == 0) + { + this.Mask = 1 << 7; + } + else + { + this.Mask <<= 8; + } + + if (this.UnreadBits >= n) + { + return JpegDecoderCore.ErrorCodes.NoError; + } + } + } + + internal int ReceiveExtend(byte t, JpegDecoderCore decoder) + { + if (this.UnreadBits < t) + { + var errorCode = this.EnsureNBits(t, decoder); + if (errorCode != JpegDecoderCore.ErrorCodes.NoError) + { + throw new JpegDecoderCore.MissingFF00Exception(); + } + } + + this.UnreadBits -= t; + this.Mask >>= t; + int s = 1 << t; + int x = (int)((this.Accumulator >> this.UnreadBits) & (s - 1)); + + if (x < (s >> 1)) + { + x += ((-1) << t) + 1; + } + + return x; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Block.cs b/src/ImageSharp/Formats/Jpg/Components/Block.cs index 8bc5a861ca..4d4ad4d81e 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Block.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Block.cs @@ -2,14 +2,19 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageSharp.Formats { + using System; + using System.Buffers; + using System.Runtime.CompilerServices; + /// /// Represents an 8x8 block of coefficients to transform and encode. /// - internal class Block + internal struct Block : IDisposable { + private static readonly ArrayPool ArrayPool = ArrayPool.Create(BlockSize, 50); + /// /// Gets the size of the block. /// @@ -18,16 +23,137 @@ namespace ImageSharp.Formats /// /// The array of block data. /// - private readonly int[] data; + public int[] Data; + + public void Init() + { + // this.Data = new int[BlockSize]; + this.Data = ArrayPool.Rent(BlockSize); + } + + public static Block Create() + { + var block = new Block(); + block.Init(); + return block; + } + + public static Block[] CreateArray(int size) + { + Block[] result = new Block[size]; + for (int i = 0; i < result.Length; i++) + { + result[i].Init(); + } + + return result; + } + + public bool IsInitialized => this.Data != null; /// - /// Initializes a new instance of the class. + /// Gets the pixel data at the given block index. /// - public Block() + /// The index of the data to return. + /// + /// The . + /// + public int this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return this.Data[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.Data[index] = value; + } + } + + // TODO: Refactor Block.Dispose() callers to always use 'using' or 'finally' statement! + public void Dispose() + { + if (this.Data != null) + { + ArrayPool.Return(this.Data, true); + this.Data = null; + } + } + + public static void DisposeAll(Block[] blocks) { - this.data = new int[BlockSize]; + for (int i = 0; i < blocks.Length; i++) + { + blocks[i].Dispose(); + } } + public void Clear() + { + for (int i = 0; i < this.Data.Length; i++) + { + this.Data[i] = 0; + } + } + + public Block Clone() + { + Block clone = Create(); + Array.Copy(this.Data, clone.Data, BlockSize); + return clone; + } + } + + /// + /// TODO: Should be removed, when JpegEncoderCore is refactored to use Block8x8F + /// Temporal class to make refactoring easier. + /// 1. Refactor Block -> BlockF + /// 2. Test + /// 3. Refactor BlockF -> Block8x8F + /// + internal struct BlockF : IDisposable + { + private static readonly ArrayPool ArrayPool = ArrayPool.Create(BlockSize, 50); + + /// + /// Size of the block. + /// + public const int BlockSize = 64; + + /// + /// The array of block data. + /// + public float[] Data; + + public void Init() + { + // this.Data = new int[BlockSize]; + this.Data = ArrayPool.Rent(BlockSize); + } + + public static BlockF Create() + { + var block = new BlockF(); + block.Init(); + return block; + } + + public static BlockF[] CreateArray(int size) + { + BlockF[] result = new BlockF[size]; + for (int i = 0; i < result.Length; i++) + { + result[i].Init(); + } + + return result; + } + + public bool IsInitialized => this.Data != null; + /// /// Gets the pixel data at the given block index. /// @@ -35,10 +161,52 @@ namespace ImageSharp.Formats /// /// The . /// - public int this[int index] + public float this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return this.Data[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.Data[index] = value; + } + } + + // TODO: Refactor Block.Dispose() callers to always use 'using' or 'finally' statement! + public void Dispose() + { + if (this.Data != null) + { + ArrayPool.Return(this.Data, true); + this.Data = null; + } + } + + public static void DisposeAll(BlockF[] blocks) + { + for (int i = 0; i < blocks.Length; i++) + { + blocks[i].Dispose(); + } + } + + public void Clear() + { + for (int i = 0; i < this.Data.Length; i++) + { + this.Data[i] = 0; + } + } + + public BlockF Clone() { - get { return this.data[index]; } - set { this.data[index] = value; } + BlockF clone = Create(); + Array.Copy(this.Data, clone.Data, BlockSize); + return clone; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Block8x8F.Generated.cs b/src/ImageSharp/Formats/Jpg/Components/Block8x8F.Generated.cs new file mode 100644 index 0000000000..3b48dc572d --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Block8x8F.Generated.cs @@ -0,0 +1,52 @@ +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + + +namespace ImageSharp.Formats +{ + internal partial struct Block8x8F + { + private static readonly Vector4 CMin4 = new Vector4(-128f); + private static readonly Vector4 CMax4 = new Vector4(127f); + private static readonly Vector4 COff4 = new Vector4(128f); + + /// + /// Transpose the block into d + /// + /// Destination + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void TransposeInto(ref Block8x8F d) + { + d.V0L.X = V0L.X; d.V1L.X = V0L.Y; d.V2L.X = V0L.Z; d.V3L.X = V0L.W; d.V4L.X = V0R.X; d.V5L.X = V0R.Y; d.V6L.X = V0R.Z; d.V7L.X = V0R.W; + d.V0L.Y = V1L.X; d.V1L.Y = V1L.Y; d.V2L.Y = V1L.Z; d.V3L.Y = V1L.W; d.V4L.Y = V1R.X; d.V5L.Y = V1R.Y; d.V6L.Y = V1R.Z; d.V7L.Y = V1R.W; + d.V0L.Z = V2L.X; d.V1L.Z = V2L.Y; d.V2L.Z = V2L.Z; d.V3L.Z = V2L.W; d.V4L.Z = V2R.X; d.V5L.Z = V2R.Y; d.V6L.Z = V2R.Z; d.V7L.Z = V2R.W; + d.V0L.W = V3L.X; d.V1L.W = V3L.Y; d.V2L.W = V3L.Z; d.V3L.W = V3L.W; d.V4L.W = V3R.X; d.V5L.W = V3R.Y; d.V6L.W = V3R.Z; d.V7L.W = V3R.W; + d.V0R.X = V4L.X; d.V1R.X = V4L.Y; d.V2R.X = V4L.Z; d.V3R.X = V4L.W; d.V4R.X = V4R.X; d.V5R.X = V4R.Y; d.V6R.X = V4R.Z; d.V7R.X = V4R.W; + d.V0R.Y = V5L.X; d.V1R.Y = V5L.Y; d.V2R.Y = V5L.Z; d.V3R.Y = V5L.W; d.V4R.Y = V5R.X; d.V5R.Y = V5R.Y; d.V6R.Y = V5R.Z; d.V7R.Y = V5R.W; + d.V0R.Z = V6L.X; d.V1R.Z = V6L.Y; d.V2R.Z = V6L.Z; d.V3R.Z = V6L.W; d.V4R.Z = V6R.X; d.V5R.Z = V6R.Y; d.V6R.Z = V6R.Z; d.V7R.Z = V6R.W; + d.V0R.W = V7L.X; d.V1R.W = V7L.Y; d.V2R.W = V7L.Z; d.V3R.W = V7L.W; d.V4R.W = V7R.X; d.V5R.W = V7R.Y; d.V6R.W = V7R.Z; d.V7R.W = V7R.W; + } + + /// + /// Level shift by +128, clip to [0, 255] + /// + /// Destination + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void TransformByteConvetibleColorValuesInto(ref Block8x8F d) + { + d.V0L = Vector4.Max(Vector4.Min(V0L, CMax4), CMin4) + COff4;d.V0R = Vector4.Max(Vector4.Min(V0R, CMax4), CMin4) + COff4; + d.V1L = Vector4.Max(Vector4.Min(V1L, CMax4), CMin4) + COff4;d.V1R = Vector4.Max(Vector4.Min(V1R, CMax4), CMin4) + COff4; + d.V2L = Vector4.Max(Vector4.Min(V2L, CMax4), CMin4) + COff4;d.V2R = Vector4.Max(Vector4.Min(V2R, CMax4), CMin4) + COff4; + d.V3L = Vector4.Max(Vector4.Min(V3L, CMax4), CMin4) + COff4;d.V3R = Vector4.Max(Vector4.Min(V3R, CMax4), CMin4) + COff4; + d.V4L = Vector4.Max(Vector4.Min(V4L, CMax4), CMin4) + COff4;d.V4R = Vector4.Max(Vector4.Min(V4R, CMax4), CMin4) + COff4; + d.V5L = Vector4.Max(Vector4.Min(V5L, CMax4), CMin4) + COff4;d.V5R = Vector4.Max(Vector4.Min(V5R, CMax4), CMin4) + COff4; + d.V6L = Vector4.Max(Vector4.Min(V6L, CMax4), CMin4) + COff4;d.V6R = Vector4.Max(Vector4.Min(V6R, CMax4), CMin4) + COff4; + d.V7L = Vector4.Max(Vector4.Min(V7L, CMax4), CMin4) + COff4;d.V7R = Vector4.Max(Vector4.Min(V7R, CMax4), CMin4) + COff4; + } + + + } +} diff --git a/src/ImageSharp/Formats/Jpg/Components/Block8x8F.Generated.tt b/src/ImageSharp/Formats/Jpg/Components/Block8x8F.Generated.tt new file mode 100644 index 0000000000..951d62d914 --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Block8x8F.Generated.tt @@ -0,0 +1,80 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +<# +char[] coordz = {'X', 'Y', 'Z', 'W'}; +#> + +namespace ImageSharp.Formats +{ + internal partial struct Block8x8F + { + private static readonly Vector4 CMin4 = new Vector4(-128f); + private static readonly Vector4 CMax4 = new Vector4(127f); + private static readonly Vector4 COff4 = new Vector4(128f); + + /// + /// Transpose the block into d + /// + /// Destination + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void TransposeInto(ref Block8x8F d) + { + <# + PushIndent(" "); + + for (int i = 0; i < 8; i++) + { + char destCoord = coordz[i % 4]; + char destSide = (i / 4) % 2 == 0 ? 'L' : 'R'; + + for (int j = 0; j < 8; j++) + { + char srcCoord = coordz[j % 4]; + char srcSide = (j / 4) % 2 == 0 ? 'L' : 'R'; + + string expression = $"d.V{j}{destSide}.{destCoord} = V{i}{srcSide}.{srcCoord}; "; + Write(expression); + } + WriteLine(""); + } + PopIndent(); + #> + } + + /// + /// Level shift by +128, clip to [0, 255] + /// + /// Destination + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void TransformByteConvetibleColorValuesInto(ref Block8x8F d) + { + <# + + PushIndent(" "); + + for (int i = 0; i < 8; i++) + { + for (int j = 0; j < 2; j++) + { + char side = j == 0 ? 'L' : 'R'; + Write($"d.V{i}{side} = Vector4.Max(Vector4.Min(V{i}{side}, CMax4), CMin4) + COff4;"); + } + WriteLine(""); + } + PopIndent(); + #> + } + + + } +} diff --git a/src/ImageSharp/Formats/Jpg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpg/Components/Block8x8F.cs new file mode 100644 index 0000000000..6fefb86965 --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/Block8x8F.cs @@ -0,0 +1,405 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// ReSharper disable InconsistentNaming +namespace ImageSharp.Formats +{ + using System; + using System.Numerics; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + /// + /// DCT code Ported from https://github.com/norishigefukushima/dct_simd + /// + internal partial struct Block8x8F + { + public Vector4 V0L; + public Vector4 V0R; + + public Vector4 V1L; + public Vector4 V1R; + + public Vector4 V2L; + public Vector4 V2R; + + public Vector4 V3L; + public Vector4 V3R; + + public Vector4 V4L; + public Vector4 V4R; + + public Vector4 V5L; + public Vector4 V5R; + + public Vector4 V6L; + public Vector4 V6R; + + public Vector4 V7L; + public Vector4 V7R; + + public const int VectorCount = 16; + public const int ScalarCount = VectorCount*4; + + /// + /// Load raw 32bit floating point data from source + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void LoadFrom(MutableSpan source) + { + fixed (void* ptr = &this.V0L) + { + Marshal.Copy(source.Data, source.Offset, (IntPtr)ptr, ScalarCount); + } + } + + /// + /// Load raw 32bit floating point data from source + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void LoadFrom(Block8x8F* blockPtr, MutableSpan source) + { + Marshal.Copy(source.Data, source.Offset, (IntPtr)blockPtr, ScalarCount); + } + + /// + /// Load raw 32bit floating point data from source + /// + internal unsafe void LoadFrom(MutableSpan source) + { + fixed (Vector4* ptr = &this.V0L) + { + float* fp = (float*)ptr; + for (int i = 0; i < ScalarCount; i++) + { + fp[i] = source[i]; + } + } + } + + /// + /// Copy raw 32bit floating point data to dest + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void CopyTo(MutableSpan dest) + { + fixed (void* ptr = &this.V0L) + { + Marshal.Copy((IntPtr)ptr, dest.Data, dest.Offset, ScalarCount); + } + } + + /// + /// Copy raw 32bit floating point data to dest + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void CopyTo(float[] dest) + { + fixed (void* ptr = &this.V0L) + { + Marshal.Copy((IntPtr)ptr, dest, 0, ScalarCount); + } + } + + /// + /// Copy raw 32bit floating point data to dest + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void CopyTo(Block8x8F* blockPtr, MutableSpan dest) + { + Marshal.Copy((IntPtr)blockPtr, dest.Data, dest.Offset, ScalarCount); + } + + /// + /// Copy raw 32bit floating point data to dest + /// + internal unsafe void CopyTo(MutableSpan dest) + { + fixed (Vector4* ptr = &this.V0L) + { + float* fp = (float*)ptr; + for (int i = 0; i < ScalarCount; i++) + { + dest[i] = (int)fp[i]; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MultiplyAllInplace(Vector4 s) + { + this.V0L *= s; + this.V0R *= s; + this.V1L *= s; + this.V1R *= s; + this.V2L *= s; + this.V2R *= s; + this.V3L *= s; + this.V3R *= s; + this.V4L *= s; + this.V4R *= s; + this.V5L *= s; + this.V5R *= s; + this.V6L *= s; + this.V6R *= s; + this.V7L *= s; + this.V7R *= s; + } + + /// + /// Apply floating point IDCT transformation into dest, using a temporary block 'temp' provided by the caller (optimization) + /// + /// Destination + /// Temporary block provided by the caller + public void TransformIDCTInto(ref Block8x8F dest, ref Block8x8F temp) + { + this.TransposeInto(ref temp); + temp.IDCT8x4_LeftPart(ref dest); + temp.IDCT8x4_RightPart(ref dest); + + dest.TransposeInto(ref temp); + + temp.IDCT8x4_LeftPart(ref dest); + temp.IDCT8x4_RightPart(ref dest); + + dest.MultiplyAllInplace(c_0_125); + } + + private static readonly Vector4 c_1_175876 = new Vector4(1.175876f); + private static readonly Vector4 c_1_961571 = new Vector4(-1.961571f); + private static readonly Vector4 c_0_390181 = new Vector4(-0.390181f); + private static readonly Vector4 c_0_899976 = new Vector4(-0.899976f); + private static readonly Vector4 c_2_562915 = new Vector4(-2.562915f); + private static readonly Vector4 c_0_298631 = new Vector4(0.298631f); + private static readonly Vector4 c_2_053120 = new Vector4(2.053120f); + private static readonly Vector4 c_3_072711 = new Vector4(3.072711f); + private static readonly Vector4 c_1_501321 = new Vector4(1.501321f); + private static readonly Vector4 c_0_541196 = new Vector4(0.541196f); + private static readonly Vector4 c_1_847759 = new Vector4(-1.847759f); + private static readonly Vector4 c_0_765367 = new Vector4(0.765367f); + + private static readonly Vector4 c_0_125 = new Vector4(0.1250f); + + /// + /// Do IDCT internal operations on the left part of the block. Original source: + /// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261 + /// + /// Destination block + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void IDCT8x4_LeftPart(ref Block8x8F d) + { + Vector4 my1 = this.V1L; + Vector4 my7 = this.V7L; + Vector4 mz0 = my1 + my7; + + Vector4 my3 = this.V3L; + Vector4 mz2 = my3 + my7; + Vector4 my5 = this.V5L; + Vector4 mz1 = my3 + my5; + Vector4 mz3 = my1 + my5; + + Vector4 mz4 = ((mz0 + mz1) * c_1_175876); + + mz2 = (mz2 * c_1_961571) + mz4; + mz3 = (mz3 * c_0_390181) + mz4; + mz0 = mz0 * c_0_899976; + mz1 = mz1 * c_2_562915; + + Vector4 mb3 = (my7 * c_0_298631) + mz0 + mz2; + Vector4 mb2 = (my5 * c_2_053120) + mz1 + mz3; + Vector4 mb1 = (my3 * c_3_072711) + mz1 + mz2; + Vector4 mb0 = (my1 * c_1_501321) + mz0 + mz3; + + Vector4 my2 = this.V2L; + Vector4 my6 = this.V6L; + mz4 = (my2 + my6) * c_0_541196; + Vector4 my0 = this.V0L; + Vector4 my4 = this.V4L; + mz0 = my0 + my4; + mz1 = my0 - my4; + + mz2 = mz4 + (my6 * c_1_847759); + mz3 = mz4 + (my2 * c_0_765367); + + my0 = mz0 + mz3; + my3 = mz0 - mz3; + my1 = mz1 + mz2; + my2 = mz1 - mz2; + + d.V0L = my0 + mb0; + d.V7L = my0 - mb0; + d.V1L = my1 + mb1; + d.V6L = my1 - mb1; + d.V2L = my2 + mb2; + d.V5L = my2 - mb2; + d.V3L = my3 + mb3; + d.V4L = my3 - mb3; + } + + /// + /// Do IDCT internal operations on the right part of the block. + /// Original source: + /// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void IDCT8x4_RightPart(ref Block8x8F d) + { + Vector4 my1 = this.V1R; + Vector4 my7 = this.V7R; + Vector4 mz0 = my1 + my7; + + Vector4 my3 = this.V3R; + Vector4 mz2 = my3 + my7; + Vector4 my5 = this.V5R; + Vector4 mz1 = my3 + my5; + Vector4 mz3 = my1 + my5; + + Vector4 mz4 = (mz0 + mz1) * c_1_175876; + + mz2 = (mz2 * c_1_961571) + mz4; + mz3 = (mz3 * c_0_390181) + mz4; + mz0 = mz0 * c_0_899976; + mz1 = mz1 * c_2_562915; + + Vector4 mb3 = (my7 * c_0_298631) + mz0 + mz2; + Vector4 mb2 = (my5 * c_2_053120) + mz1 + mz3; + Vector4 mb1 = (my3 * c_3_072711) + mz1 + mz2; + Vector4 mb0 = (my1 * c_1_501321) + mz0 + mz3; + + Vector4 my2 = this.V2R; + Vector4 my6 = this.V6R; + mz4 = (my2 + my6) * c_0_541196; + Vector4 my0 = this.V0R; + Vector4 my4 = this.V4R; + mz0 = my0 + my4; + mz1 = my0 - my4; + + mz2 = mz4 + (my6 * c_1_847759); + mz3 = mz4 + (my2 * c_0_765367); + + my0 = mz0 + mz3; + my3 = mz0 - mz3; + my1 = mz1 + mz2; + my2 = mz1 - mz2; + + d.V0R = my0 + mb0; + d.V7R = my0 - mb0; + d.V1R = my1 + mb1; + d.V6R = my1 - mb1; + d.V2R = my2 + mb2; + d.V5R = my2 - mb2; + d.V3R = my3 + mb3; + d.V4R = my3 - mb3; + } + + public unsafe float this[int idx] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + fixed (Block8x8F* p = &this) + { + float* fp = (float*)p; + return fp[idx]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + fixed (Block8x8F* p = &this) + { + float* fp = (float*)p; + fp[idx] = value; + } + } + } + + /// + /// Pointer-based "Indexer" (getter part) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe float GetScalarAt(Block8x8F* blockPtr, int idx) + { + float* fp = (float*)blockPtr; + return fp[idx]; + } + + /// + /// Pointer-based "Indexer" (setter part) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe void SetScalarAt(Block8x8F* blockPtr, int idx, float value) + { + float* fp = (float*)blockPtr; + fp[idx] = value; + } + + /// + /// Fill the block with defaults (zeroes) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Clear() + { + // The cheapest way to do this in C#: + this = new Block8x8F(); + } + + /// + /// TODO: Should be removed when BlockF goes away + /// + /// + internal void LoadFrom(ref BlockF legacyBlock) + { + this.LoadFrom(legacyBlock.Data); + } + + /// + /// TODO: Should be removed when BlockF goes away + /// + /// + internal void CopyTo(ref BlockF legacyBlock) + { + this.CopyTo(legacyBlock.Data); + } + + /// + /// Level shift by +128, clip to [0, 255], and write to buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void CopyColorsTo(MutableSpan buffer, int stride, Block8x8F* temp) + { + this.TransformByteConvetibleColorValuesInto(ref *temp); + + float* src = (float*)temp; + for (int i = 0; i < 8; i++) + { + buffer[0] = (byte)src[0]; + buffer[1] = (byte)src[1]; + buffer[2] = (byte)src[2]; + buffer[3] = (byte)src[3]; + buffer[4] = (byte)src[4]; + buffer[5] = (byte)src[5]; + buffer[6] = (byte)src[6]; + buffer[7] = (byte)src[7]; + buffer.AddOffset(stride); + src += 8; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe void UnZig(Block8x8F* block, Block8x8F* qt, int* unzigPtr) + { + float* b = (float*)block; + float* qtp = (float*)qt; + for (int zig = 0; zig < BlockF.BlockSize; zig++) + { + float* unzigPos = b + unzigPtr[zig]; + float val = *unzigPos; + val *= qtp[zig]; + *unzigPos = val; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Bytes.cs b/src/ImageSharp/Formats/Jpg/Components/Bytes.cs index d4159e9963..32a767d586 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Bytes.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Bytes.cs @@ -2,25 +2,28 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageSharp.Formats { + using System; + using System.Buffers; + using System.IO; + using System.Runtime.CompilerServices; + /// /// Bytes is a byte buffer, similar to a stream, except that it /// has to be able to unread more than 1 byte, due to byte stuffing. /// Byte stuffing is specified in section F.1.2.3. /// - internal class Bytes + internal struct Bytes : IDisposable { + private static readonly ArrayPool ArrayPool = ArrayPool.Create(4096, 50); + /// - /// Initializes a new instance of the class. + /// Creates a new instance of the , and initializes it's buffer. /// - public Bytes() + public static Bytes Create() { - this.Buffer = new byte[4096]; - this.I = 0; - this.J = 0; - this.UnreadableBytes = 0; + return new Bytes { Buffer = ArrayPool.Rent(4096) }; } /// @@ -28,16 +31,128 @@ namespace ImageSharp.Formats /// buffer[i:j] are the buffered bytes read from the underlying /// stream that haven't yet been passed further on. /// - public byte[] Buffer { get; set; } + public byte[] Buffer; - public int I { get; set; } + public int I; - public int J { get; set; } + public int J; /// /// Gets or sets the unreadable bytes. The number of bytes to back up i after /// overshooting. It can be 0, 1 or 2. /// - public int UnreadableBytes { get; set; } + public int UnreadableBytes; + + public void Dispose() + { + if (this.Buffer != null) ArrayPool.Return(this.Buffer); + this.Buffer = null; + } + + /// + /// ReadByteStuffedByte is like ReadByte but is for byte-stuffed Huffman data. + /// + /// The + internal byte ReadByteStuffedByte(Stream inputStream, out JpegDecoderCore.ErrorCodes errorCode) + { + byte x; + + errorCode = JpegDecoderCore.ErrorCodes.NoError; + + // Take the fast path if bytes.buf contains at least two bytes. + if (this.I + 2 <= this.J) + { + x = this.Buffer[this.I]; + this.I++; + this.UnreadableBytes = 1; + if (x != JpegConstants.Markers.XFF) + { + return x; + } + + if (this.Buffer[this.I] != 0x00) + { + errorCode = JpegDecoderCore.ErrorCodes.MissingFF00; + return 0; + + // throw new MissingFF00Exception(); + } + + this.I++; + this.UnreadableBytes = 2; + return JpegConstants.Markers.XFF; + } + + this.UnreadableBytes = 0; + + x = this.ReadByte(inputStream); + this.UnreadableBytes = 1; + if (x != JpegConstants.Markers.XFF) + { + return x; + } + + x = this.ReadByte(inputStream); + this.UnreadableBytes = 2; + if (x != 0x00) + { + errorCode = JpegDecoderCore.ErrorCodes.MissingFF00; + return 0; + + // throw new MissingFF00Exception(); + } + + return JpegConstants.Markers.XFF; + } + + /// + /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal byte ReadByte(Stream inputStream) + { + while (this.I == this.J) + { + this.Fill(inputStream); + } + + byte x = this.Buffer[this.I]; + this.I++; + this.UnreadableBytes = 0; + return x; + } + + /// + /// Fills up the bytes buffer from the underlying stream. + /// It should only be called when there are no unread bytes in bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Fill(Stream inputStream) + { + if (this.I != this.J) + { + throw new ImageFormatException("Fill called when unread bytes exist."); + } + + // Move the last 2 bytes to the start of the buffer, in case we need + // to call UnreadByteStuffedByte. + if (this.J > 2) + { + this.Buffer[0] = this.Buffer[this.J - 2]; + this.Buffer[1] = this.Buffer[this.J - 1]; + this.I = 2; + this.J = 2; + } + + // Fill in the rest of the buffer. + int n = inputStream.Read(this.Buffer, this.J, this.Buffer.Length - this.J); + if (n == 0) + { + throw new JpegDecoderCore.EOFException(); + } + + this.J += n; + } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/Component.cs b/src/ImageSharp/Formats/Jpg/Components/Component.cs index f56b6d513e..f70dbff3f4 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Component.cs @@ -8,26 +8,26 @@ namespace ImageSharp.Formats /// /// Represents a single color component /// - internal class Component + internal struct Component { /// /// Gets or sets the horizontal sampling factor. /// - public int HorizontalFactor { get; set; } + public int HorizontalFactor; /// /// Gets or sets the vertical sampling factor. /// - public int VerticalFactor { get; set; } + public int VerticalFactor; /// /// Gets or sets the identifier /// - public byte Identifier { get; set; } + public byte Identifier; /// /// Gets or sets the quantization table destination selector. /// - public byte Selector { get; set; } + public byte Selector; } } diff --git a/src/ImageSharp/Formats/Jpg/Components/FDCT.cs b/src/ImageSharp/Formats/Jpg/Components/FDCT.cs index cd27b9e73c..13ce80f559 100644 --- a/src/ImageSharp/Formats/Jpg/Components/FDCT.cs +++ b/src/ImageSharp/Formats/Jpg/Components/FDCT.cs @@ -45,7 +45,7 @@ namespace ImageSharp.Formats /// Performs a forward DCT on an 8x8 block of coefficients, including a level shift. /// /// The block of coefficients. - public static void Transform(Block block) + public static void Transform(ref Block block) { // Pass 1: process rows. for (int y = 0; y < 8; y++) diff --git a/src/ImageSharp/Formats/Jpg/Components/Huffman.cs b/src/ImageSharp/Formats/Jpg/Components/Huffman.cs index 2c38cfd380..7a6f031e86 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Huffman.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Huffman.cs @@ -2,34 +2,36 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageSharp.Formats { + using System; + using System.Buffers; + /// /// Represents a Huffman tree /// - internal class Huffman + internal struct Huffman : IDisposable { - /// - /// Initializes a new instance of the class. - /// - /// The log-2 size of the Huffman decoder's look-up table. - /// The maximum (inclusive) number of codes in a Huffman tree. - /// The maximum (inclusive) number of bits in a Huffman code. - public Huffman(int lutSize, int maxNCodes, int maxCodeLength) + private static ArrayPool UshortBuffer = ArrayPool.Create(1 << JpegDecoderCore.LutSize, 50); + + private static ArrayPool ByteBuffer = ArrayPool.Create(JpegDecoderCore.MaxNCodes, 50); + + private static readonly ArrayPool IntBuffer = ArrayPool.Create(JpegDecoderCore.MaxCodeLength, 50); + + public void Init(int lutSize, int maxNCodes, int maxCodeLength) { - this.Lut = new ushort[1 << lutSize]; - this.Values = new byte[maxNCodes]; - this.MinCodes = new int[maxCodeLength]; - this.MaxCodes = new int[maxCodeLength]; - this.Indices = new int[maxCodeLength]; - this.Length = 0; + this.Lut = UshortBuffer.Rent(1 << lutSize); + this.Values = ByteBuffer.Rent(maxNCodes); + this.MinCodes = IntBuffer.Rent(maxCodeLength); + this.MaxCodes = IntBuffer.Rent(maxCodeLength); + this.Indices = IntBuffer.Rent(maxCodeLength); + } /// /// Gets or sets the number of codes in the tree. /// - public int Length { get; set; } + public int Length; /// /// Gets the look-up table for the next LutSize bits in the bit-stream. @@ -37,28 +39,37 @@ namespace ImageSharp.Formats /// are 1 plus the code length, or 0 if the value is too large to fit in /// lutSize bits. /// - public ushort[] Lut { get; } + public ushort[] Lut; /// /// Gets the the decoded values, sorted by their encoding. /// - public byte[] Values { get; } + public byte[] Values; /// /// Gets the array of minimum codes. /// MinCodes[i] is the minimum code of length i, or -1 if there are no codes of that length. /// - public int[] MinCodes { get; } + public int[] MinCodes; /// /// Gets the array of maximum codes. /// MaxCodes[i] is the maximum code of length i, or -1 if there are no codes of that length. /// - public int[] MaxCodes { get; } + public int[] MaxCodes; /// /// Gets the array of indices. Indices[i] is the index into Values of MinCodes[i]. /// - public int[] Indices { get; } + public int[] Indices; + + public void Dispose() + { + UshortBuffer.Return(this.Lut, true); + ByteBuffer.Return(this.Values, true); + IntBuffer.Return(this.MinCodes, true); + IntBuffer.Return(this.MaxCodes, true); + IntBuffer.Return(this.Indices, true); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/Components/IDCT.cs b/src/ImageSharp/Formats/Jpg/Components/IDCT.cs index bc145779a2..b3277e0278 100644 --- a/src/ImageSharp/Formats/Jpg/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpg/Components/IDCT.cs @@ -39,7 +39,7 @@ namespace ImageSharp.Formats /// ASSP, Vol. ASSP- 32, pp. 803-816, Aug. 1984. /// /// The source block of coefficients - public static void Transform(Block src) + public static void Transform(ref Block src) { // Horizontal 1-D IDCT. for (int y = 0; y < 8; y++) @@ -165,5 +165,6 @@ namespace ImageSharp.Formats src[56 + x] = (y7 - y1) >> 14; } } + } } diff --git a/src/ImageSharp/Formats/Jpg/Components/MutableSpan.cs b/src/ImageSharp/Formats/Jpg/Components/MutableSpan.cs new file mode 100644 index 0000000000..492f28f5b4 --- /dev/null +++ b/src/ImageSharp/Formats/Jpg/Components/MutableSpan.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + using System.Numerics; + using System.Runtime.CompilerServices; + + /// + /// Like corefxlab Span, but with an AddOffset() method for efficiency. + /// TODO: When Span will be official, consider replacing this class! + /// + /// + /// https://github.com/dotnet/corefxlab/blob/master/src/System.Slices/System/Span.cs + /// + /// + internal struct MutableSpan + { + public T[] Data; + + public int Offset; + + public int TotalCount => this.Data.Length - this.Offset; + + public MutableSpan(int size, int offset = 0) + { + this.Data = new T[size]; + this.Offset = offset; + } + + public MutableSpan(T[] data, int offset = 0) + { + this.Data = data; + this.Offset = offset; + } + + 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; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MutableSpan Slice(int offset) + { + return new MutableSpan(this.Data, this.Offset + offset); + } + + public static implicit operator MutableSpan(T[] data) => new MutableSpan(data, 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOffset(int offset) + { + this.Offset += offset; + } + } + + internal static class MutableSpanExtensions + { + public static MutableSpan Slice(this T[] array, int offset) => new MutableSpan(array, offset); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SaveTo(this MutableSpan data, ref Vector4 v) + { + v.X = data[0]; + v.Y = data[1]; + v.Z = data[2]; + v.W = data[3]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SaveTo(this MutableSpan data, ref Vector4 v) + { + v.X = data[0]; + v.Y = data[1]; + v.Z = data[2]; + v.W = data[3]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LoadFrom(this MutableSpan data, ref Vector4 v) + { + data[0] = v.X; + data[1] = v.Y; + data[2] = v.Z; + data[3] = v.W; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LoadFrom(this MutableSpan data, ref Vector4 v) + { + data[0] = (int)v.X; + data[1] = (int)v.Y; + data[2] = (int)v.Z; + data[3] = (int)v.W; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpg/JpegDecoder.cs index 666f1b35a0..d3c64a9c06 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoder.cs @@ -83,8 +83,10 @@ namespace ImageSharp.Formats Guard.NotNull(image, "image"); Guard.NotNull(stream, "stream"); - JpegDecoderCore decoder = new JpegDecoderCore(); - decoder.Decode(image, stream, false); + using (JpegDecoderCore decoder = new JpegDecoderCore()) + { + decoder.Decode(image, stream, false); + } } /// diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index 332dcec488..446da29dbd 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -7,27 +7,29 @@ namespace ImageSharp.Formats { using System; using System.IO; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; using System.Threading.Tasks; /// /// Performs the jpeg decoding operation. /// - internal class JpegDecoderCore + internal unsafe class JpegDecoderCore : IDisposable { /// /// The maximum (inclusive) number of bits in a Huffman code. /// - private const int MaxCodeLength = 16; + internal const int MaxCodeLength = 16; /// /// The maximum (inclusive) number of codes in a Huffman tree. /// - private const int MaxNCodes = 256; + internal const int MaxNCodes = 256; /// /// The log-2 size of the Huffman decoder's look-up table. /// - private const int LutSize = 8; + internal const int LutSize = 8; /// /// The maximum number of color components @@ -44,6 +46,8 @@ namespace ImageSharp.Formats /// private const int MaxTh = 3; + private const int ThRowSize = MaxTh + 1; + /// /// The maximum number of quantization tables /// @@ -65,12 +69,12 @@ namespace ImageSharp.Formats /// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). /// private static readonly int[] Unzig = - { - 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, - 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, - 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, - 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, - }; + { + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, + 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, + 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + }; /// /// The component array @@ -80,17 +84,18 @@ namespace ImageSharp.Formats /// /// Saved state between progressive-mode scans. /// - private readonly Block[][] progCoeffs; + private readonly Block8x8F[][] progCoeffs; /// /// The huffman trees /// - private readonly Huffman[,] huffmanTrees; + //private readonly Huffman[,] huffmanTrees; + private readonly Huffman[] huffmanTrees; /// /// Quantization tables, in zigzag order. /// - private readonly Block[] quantizationTables; + private readonly Block8x8F[] quantizationTables; /// /// A temporary buffer for holding pixels @@ -100,7 +105,7 @@ namespace ImageSharp.Formats /// /// The byte buffer. /// - private readonly Bytes bytes; + internal Bytes bytes; /// /// The image width @@ -130,12 +135,12 @@ namespace ImageSharp.Formats /// /// The input stream. /// - private Stream inputStream; + internal Stream inputStream; /// /// Holds the unprocessed bits that have been taken from the byte-stream. /// - private Bits bits; + internal Bits bits; /// /// The array of keyline pixels in a CMYK image @@ -187,37 +192,32 @@ namespace ImageSharp.Formats /// private short verticalResolution; + private int blockIndex; + /// /// Initializes a new instance of the class. /// public JpegDecoderCore() { - this.huffmanTrees = new Huffman[MaxTc + 1, MaxTh + 1]; - this.quantizationTables = new Block[MaxTq + 1]; - this.temp = new byte[2 * Block.BlockSize]; + //this.huffmanTrees = new Huffman[MaxTc + 1, MaxTh + 1]; + this.huffmanTrees = new Huffman[(MaxTc + 1) * (MaxTh + 1)]; + + this.quantizationTables = new Block8x8F[MaxTq + 1]; + this.temp = new byte[2 * BlockF.BlockSize]; this.componentArray = new Component[MaxComponents]; - this.progCoeffs = new Block[MaxComponents][]; + this.progCoeffs = new Block8x8F[MaxComponents][]; this.bits = new Bits(); - this.bytes = new Bytes(); + this.bytes = Bytes.Create(); // TODO: This looks like it could be static. + for (int i = 0; i < MaxTc + 1; i++) { for (int j = 0; j < MaxTh + 1; j++) { - this.huffmanTrees[i, j] = new Huffman(LutSize, MaxNCodes, MaxCodeLength); + this.huffmanTrees[i * ThRowSize + j].Init(LutSize, MaxNCodes, MaxCodeLength); } } - - for (int i = 0; i < this.quantizationTables.Length; i++) - { - this.quantizationTables[i] = new Block(); - } - - for (int i = 0; i < this.componentArray.Length; i++) - { - this.componentArray[i] = new Component(); - } } /// @@ -230,8 +230,7 @@ namespace ImageSharp.Formats /// The stream, where the image should be. /// Whether to decode metadata only. public void Decode(Image image, Stream stream, bool configOnly) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { this.inputStream = stream; @@ -369,7 +368,8 @@ namespace ImageSharp.Formats this.ProcessApp14Marker(remaining); break; default: - if ((JpegConstants.Markers.APP0 <= marker && marker <= JpegConstants.Markers.APP15) || marker == JpegConstants.Markers.COM) + if ((JpegConstants.Markers.APP0 <= marker && marker <= JpegConstants.Markers.APP15) + || marker == JpegConstants.Markers.COM) { this.Skip(remaining); } @@ -419,60 +419,6 @@ namespace ImageSharp.Formats } } - /// - /// Reads bytes from the byte buffer to ensure that bits.UnreadBits is at - /// least n. For best performance (avoiding function calls inside hot loops), - /// the caller is the one responsible for first checking that bits.UnreadBits < n. - /// - /// The number of bits to ensure. - private void EnsureNBits(int n) - { - while (true) - { - byte c = this.ReadByteStuffedByte(); - this.bits.Accumulator = (this.bits.Accumulator << 8) | c; - this.bits.UnreadBits += 8; - if (this.bits.Mask == 0) - { - this.bits.Mask = 1 << 7; - } - else - { - this.bits.Mask <<= 8; - } - - if (this.bits.UnreadBits >= n) - { - break; - } - } - } - - /// - /// The composition of RECEIVE and EXTEND, specified in section F.2.2.1. - /// - /// The byte - /// The - private int ReceiveExtend(byte t) - { - if (this.bits.UnreadBits < t) - { - this.EnsureNBits(t); - } - - this.bits.UnreadBits -= t; - this.bits.Mask >>= t; - int s = 1 << t; - int x = (int)((this.bits.Accumulator >> this.bits.UnreadBits) & (s - 1)); - - if (x < (s >> 1)) - { - x += ((-1) << t) + 1; - } - - return x; - } - /// /// Processes a Define Huffman Table marker, and initializes a huffman /// struct from its contents. Specified in section B.2.4.2. @@ -501,92 +447,95 @@ namespace ImageSharp.Formats throw new ImageFormatException("Bad Th value"); } - Huffman huffman = this.huffmanTrees[tc, th]; + this.ProcessDefineHuffmanTablesMarkerLoop(ref this.huffmanTrees[tc * ThRowSize + th], ref remaining); + } + } - // Read nCodes and huffman.Valuess (and derive h.Length). - // nCodes[i] is the number of codes with code length i. - // h.Length is the total number of codes. - huffman.Length = 0; + private void ProcessDefineHuffmanTablesMarkerLoop(ref Huffman huffman, ref int remaining) + { + // Read nCodes and huffman.Valuess (and derive h.Length). + // nCodes[i] is the number of codes with code length i. + // h.Length is the total number of codes. + huffman.Length = 0; - int[] ncodes = new int[MaxCodeLength]; - for (int i = 0; i < ncodes.Length; i++) - { - ncodes[i] = this.temp[i + 1]; - huffman.Length += ncodes[i]; - } + int[] ncodes = new int[MaxCodeLength]; + for (int i = 0; i < ncodes.Length; i++) + { + ncodes[i] = this.temp[i + 1]; + huffman.Length += ncodes[i]; + } - if (huffman.Length == 0) - { - throw new ImageFormatException("Huffman table has zero length"); - } + if (huffman.Length == 0) + { + throw new ImageFormatException("Huffman table has zero length"); + } - if (huffman.Length > MaxNCodes) - { - throw new ImageFormatException("Huffman table has excessive length"); - } + if (huffman.Length > MaxNCodes) + { + throw new ImageFormatException("Huffman table has excessive length"); + } - remaining -= huffman.Length + 17; - if (remaining < 0) - { - throw new ImageFormatException("DHT has wrong length"); - } + remaining -= huffman.Length + 17; + if (remaining < 0) + { + throw new ImageFormatException("DHT has wrong length"); + } - this.ReadFull(huffman.Values, 0, huffman.Length); + this.ReadFull(huffman.Values, 0, huffman.Length); - // Derive the look-up table. - for (int i = 0; i < huffman.Lut.Length; i++) - { - huffman.Lut[i] = 0; - } + // Derive the look-up table. + for (int i = 0; i < huffman.Lut.Length; i++) + { + huffman.Lut[i] = 0; + } + + uint x = 0, code = 0; - uint x = 0, code = 0; + for (int i = 0; i < LutSize; i++) + { + code <<= 1; - for (int i = 0; i < LutSize; i++) + for (int j = 0; j < ncodes[i]; j++) { - code <<= 1; + // The codeLength is 1+i, so shift code by 8-(1+i) to + // calculate the high bits for every 8-bit sequence + // whose codeLength's high bits matches code. + // The high 8 bits of lutValue are the encoded value. + // The low 8 bits are 1 plus the codeLength. + byte base2 = (byte)(code << (7 - i)); + ushort lutValue = (ushort)((huffman.Values[x] << 8) | (2 + i)); - for (int j = 0; j < ncodes[i]; j++) + for (int k = 0; k < 1 << (7 - i); k++) { - // The codeLength is 1+i, so shift code by 8-(1+i) to - // calculate the high bits for every 8-bit sequence - // whose codeLength's high bits matches code. - // The high 8 bits of lutValue are the encoded value. - // The low 8 bits are 1 plus the codeLength. - byte base2 = (byte)(code << (7 - i)); - ushort lutValue = (ushort)((huffman.Values[x] << 8) | (2 + i)); - - for (int k = 0; k < 1 << (7 - i); k++) - { - huffman.Lut[base2 | k] = lutValue; - } - - code++; - x++; + huffman.Lut[base2 | k] = lutValue; } + + code++; + x++; } + } - // Derive minCodes, maxCodes, and indices. - int c = 0, index = 0; - for (int i = 0; i < ncodes.Length; i++) + // Derive minCodes, maxCodes, and indices. + int c = 0, index = 0; + for (int i = 0; i < ncodes.Length; i++) + { + int nc = ncodes[i]; + if (nc == 0) { - int nc = ncodes[i]; - if (nc == 0) - { - huffman.MinCodes[i] = -1; - huffman.MaxCodes[i] = -1; - huffman.Indices[i] = -1; - } - else - { - huffman.MinCodes[i] = c; - huffman.MaxCodes[i] = c + nc - 1; - huffman.Indices[i] = index; - c += nc; - index += nc; - } - - c <<= 1; + huffman.MinCodes[i] = -1; + huffman.MaxCodes[i] = -1; + huffman.Indices[i] = -1; + } + else + { + huffman.MinCodes[i] = c; + huffman.MaxCodes[i] = c + nc - 1; + huffman.Indices[i] = index; + c += nc; + index += nc; } + + c <<= 1; } } @@ -595,8 +544,10 @@ namespace ImageSharp.Formats /// /// The huffman value /// The - private byte DecodeHuffman(Huffman huffman) + private byte DecodeHuffman(ref Huffman huffman) { + // Copy stuff to the stack: + if (huffman.Length == 0) { throw new ImageFormatException("Uninitialized Huffman table"); @@ -604,10 +555,10 @@ namespace ImageSharp.Formats if (this.bits.UnreadBits < 8) { - try - { - this.EnsureNBits(8); + var errorCode = this.bits.EnsureNBits(8, this); + if (errorCode == ErrorCodes.NoError) + { ushort v = huffman.Lut[(this.bits.Accumulator >> (this.bits.UnreadBits - LutSize)) & 0xff]; if (v != 0) @@ -618,19 +569,9 @@ namespace ImageSharp.Formats return (byte)(v >> 8); } } - catch (MissingFF00Exception) - { - if (this.bytes.UnreadableBytes != 0) - { - this.UnreadByteStuffedByte(); - } - } - catch (ShortHuffmanDataException) + else { - if (this.bytes.UnreadableBytes != 0) - { - this.UnreadByteStuffedByte(); - } + this.UnreadByteStuffedByte(); } } @@ -639,7 +580,11 @@ namespace ImageSharp.Formats { if (this.bits.UnreadBits == 0) { - this.EnsureNBits(1); + var errorCode = this.bits.EnsureNBits(1, this); + if (errorCode != ErrorCodes.NoError) + { + throw new MissingFF00Exception(); + } } if ((this.bits.Accumulator & this.bits.Mask) != 0) @@ -669,7 +614,11 @@ namespace ImageSharp.Formats { if (this.bits.UnreadBits == 0) { - this.EnsureNBits(1); + var errorCode = this.bits.EnsureNBits(1, this); + if (errorCode != ErrorCodes.NoError) + { + throw new MissingFF00Exception(); + } } bool ret = (this.bits.Accumulator & this.bits.Mask) != 0; @@ -687,7 +636,11 @@ namespace ImageSharp.Formats { if (this.bits.UnreadBits < count) { - this.EnsureNBits(count); + var errorCode = this.bits.EnsureNBits(count, this); + if (errorCode != ErrorCodes.NoError) + { + throw new MissingFF00Exception(); + } } uint ret = this.bits.Accumulator >> (this.bits.UnreadBits - count); @@ -697,37 +650,6 @@ namespace ImageSharp.Formats return ret; } - /// - /// Fills up the bytes buffer from the underlying stream. - /// It should only be called when there are no unread bytes in bytes. - /// - private void Fill() - { - if (this.bytes.I != this.bytes.J) - { - throw new ImageFormatException("Fill called when unread bytes exist."); - } - - // Move the last 2 bytes to the start of the buffer, in case we need - // to call UnreadByteStuffedByte. - if (this.bytes.J > 2) - { - this.bytes.Buffer[0] = this.bytes.Buffer[this.bytes.J - 2]; - this.bytes.Buffer[1] = this.bytes.Buffer[this.bytes.J - 1]; - this.bytes.I = 2; - this.bytes.J = 2; - } - - // Fill in the rest of the buffer. - int n = this.inputStream.Read(this.bytes.Buffer, this.bytes.J, this.bytes.Buffer.Length - this.bytes.J); - if (n == 0) - { - throw new EOFException(); - } - - this.bytes.J += n; - } - /// /// Undoes the most recent ReadByteStuffedByte call, /// giving a byte of data back from bits to bytes. The Huffman look-up table @@ -751,65 +673,10 @@ namespace ImageSharp.Formats /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. /// /// The - private byte ReadByte() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal byte ReadByte() { - while (this.bytes.I == this.bytes.J) - { - this.Fill(); - } - - byte x = this.bytes.Buffer[this.bytes.I]; - this.bytes.I++; - this.bytes.UnreadableBytes = 0; - return x; - } - - /// - /// ReadByteStuffedByte is like ReadByte but is for byte-stuffed Huffman data. - /// - /// The - private byte ReadByteStuffedByte() - { - byte x; - - // Take the fast path if bytes.buf contains at least two bytes. - if (this.bytes.I + 2 <= this.bytes.J) - { - x = this.bytes.Buffer[this.bytes.I]; - this.bytes.I++; - this.bytes.UnreadableBytes = 1; - if (x != JpegConstants.Markers.XFF) - { - return x; - } - - if (this.bytes.Buffer[this.bytes.I] != 0x00) - { - throw new MissingFF00Exception(); - } - - this.bytes.I++; - this.bytes.UnreadableBytes = 2; - return JpegConstants.Markers.XFF; - } - - this.bytes.UnreadableBytes = 0; - - x = this.ReadByte(); - this.bytes.UnreadableBytes = 1; - if (x != JpegConstants.Markers.XFF) - { - return x; - } - - x = this.ReadByte(); - this.bytes.UnreadableBytes = 2; - if (x != 0x00) - { - throw new MissingFF00Exception(); - } - - return JpegConstants.Markers.XFF; + return this.bytes.ReadByte(this.inputStream); } /// @@ -846,7 +713,7 @@ namespace ImageSharp.Formats length -= this.bytes.J - this.bytes.I; this.bytes.I += this.bytes.J - this.bytes.I; - this.Fill(); + this.bytes.Fill(this.inputStream); } } } @@ -883,7 +750,7 @@ namespace ImageSharp.Formats break; } - this.Fill(); + this.bytes.Fill(this.inputStream); } } @@ -1008,7 +875,8 @@ namespace ImageSharp.Formats case 1: { // Cb. - if (this.componentArray[0].HorizontalFactor % h != 0 || this.componentArray[0].VerticalFactor % v != 0) + if (this.componentArray[0].HorizontalFactor % h != 0 + || this.componentArray[0].VerticalFactor % v != 0) { throw new ImageFormatException("Unsupported subsampling ratio"); } @@ -1019,7 +887,8 @@ namespace ImageSharp.Formats case 2: { // Cr. - if (this.componentArray[1].HorizontalFactor != h || this.componentArray[1].VerticalFactor != v) + if (this.componentArray[1].HorizontalFactor != h + || this.componentArray[1].VerticalFactor != v) { throw new ImageFormatException("Unsupported subsampling ratio"); } @@ -1059,7 +928,8 @@ namespace ImageSharp.Formats break; case 3: - if (this.componentArray[0].HorizontalFactor != h || this.componentArray[0].VerticalFactor != v) + if (this.componentArray[0].HorizontalFactor != h + || this.componentArray[0].VerticalFactor != v) { throw new ImageFormatException("Unsupported subsampling ratio"); } @@ -1099,32 +969,32 @@ namespace ImageSharp.Formats switch (x >> 4) { case 0: - if (remaining < Block.BlockSize) + if (remaining < BlockF.BlockSize) { done = true; break; } - remaining -= Block.BlockSize; - this.ReadFull(this.temp, 0, Block.BlockSize); + remaining -= BlockF.BlockSize; + this.ReadFull(this.temp, 0, BlockF.BlockSize); - for (int i = 0; i < Block.BlockSize; i++) + for (int i = 0; i < BlockF.BlockSize; i++) { this.quantizationTables[tq][i] = this.temp[i]; } break; case 1: - if (remaining < 2 * Block.BlockSize) + if (remaining < 2 * BlockF.BlockSize) { done = true; break; } - remaining -= 2 * Block.BlockSize; - this.ReadFull(this.temp, 0, 2 * Block.BlockSize); + remaining -= 2 * BlockF.BlockSize; + this.ReadFull(this.temp, 0, 2 * BlockF.BlockSize); - for (int i = 0; i < Block.BlockSize; i++) + for (int i = 0; i < BlockF.BlockSize; i++) { this.quantizationTables[tq][i] = (this.temp[2 * i] << 8) | this.temp[(2 * i) + 1]; } @@ -1177,11 +1047,8 @@ namespace ImageSharp.Formats 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'; + 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) { @@ -1203,8 +1070,7 @@ namespace ImageSharp.Formats /// The remaining bytes in the segment block. /// The image. private void ProcessApp1Marker(int remaining, Image image) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { if (remaining < 6) { @@ -1215,12 +1081,8 @@ namespace ImageSharp.Formats byte[] profile = new byte[remaining]; this.ReadFull(profile, 0, remaining); - if (profile[0] == 'E' && - profile[1] == 'x' && - profile[2] == 'i' && - profile[3] == 'f' && - profile[4] == '\0' && - profile[5] == '\0') + if (profile[0] == 'E' && profile[1] == 'x' && profile[2] == 'i' && profile[3] == 'f' && profile[4] == '\0' + && profile[5] == '\0') { image.ExifProfile = new ExifProfile(profile); } @@ -1243,11 +1105,8 @@ namespace ImageSharp.Formats this.ReadFull(this.temp, 0, 12); remaining -= 12; - if (this.temp[0] == 'A' && - this.temp[1] == 'd' && - this.temp[2] == 'o' && - this.temp[3] == 'b' && - this.temp[4] == 'e') + 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]; @@ -1268,12 +1127,12 @@ namespace ImageSharp.Formats /// The image height. /// The image. private void ConvertFromCmyk(int width, int height, Image image) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { if (!this.adobeTransformValid) { - throw new ImageFormatException("Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + throw new ImageFormatException( + "Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); } // If the 4-component JPEG image isn't explicitly marked as "Unknown (RGB @@ -1294,21 +1153,21 @@ namespace ImageSharp.Formats 0, height, y => - { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) { - byte yy = this.ycbcrImage.YChannel[yo + x]; - byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); - TColor packed = default(TColor); - this.PackCmyk(ref packed, yy, cb, cr, x, y); - pixels[x, y] = packed; - } - }); + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YChannel[yo + x]; + byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + + TColor packed = default(TColor); + this.PackCmyk(ref packed, yy, cb, cr, x, y); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); @@ -1324,8 +1183,7 @@ namespace ImageSharp.Formats /// The image height. /// The image. private void ConvertFromGrayScale(int width, int height, Image image) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { image.InitPixels(width, height); @@ -1336,17 +1194,17 @@ namespace ImageSharp.Formats height, Bootstrapper.Instance.ParallelOptions, y => - { - int yoff = this.grayImage.GetRowOffset(y); - for (int x = 0; x < width; x++) { - byte rgb = this.grayImage.Pixels[yoff + x]; + int yoff = this.grayImage.GetRowOffset(y); + for (int x = 0; x < width; x++) + { + byte rgb = this.grayImage.Pixels[yoff + x]; - TColor packed = default(TColor); - packed.PackFromBytes(rgb, rgb, rgb, 255); - pixels[x, y] = packed; - } - }); + TColor packed = default(TColor); + packed.PackFromBytes(rgb, rgb, rgb, 255); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); @@ -1361,8 +1219,7 @@ namespace ImageSharp.Formats /// The image height. /// The image. private void ConvertFromYCbCr(int width, int height, Image image) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; image.InitPixels(width, height); @@ -1374,21 +1231,21 @@ namespace ImageSharp.Formats height, Bootstrapper.Instance.ParallelOptions, y => - { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) { - byte yy = this.ycbcrImage.YChannel[yo + x]; - byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); - TColor packed = default(TColor); - this.PackYcbCr(ref packed, yy, cb, cr); - pixels[x, y] = packed; - } - }); + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YChannel[yo + x]; + byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + + TColor packed = default(TColor); + PackYcbCr(ref packed, yy, cb, cr); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); @@ -1403,8 +1260,7 @@ namespace ImageSharp.Formats /// The height. /// The image. private void ConvertFromRGB(int width, int height, Image image) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; image.InitPixels(width, height); @@ -1416,21 +1272,21 @@ namespace ImageSharp.Formats height, Bootstrapper.Instance.ParallelOptions, y => - { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < width; x++) { - byte red = this.ycbcrImage.YChannel[yo + x]; - byte green = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte blue = this.ycbcrImage.CrChannel[co + (x / scale)]; + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); - TColor packed = default(TColor); - packed.PackFromBytes(red, green, blue, 255); - pixels[x, y] = packed; - } - }); + for (int x = 0; x < width; x++) + { + byte red = this.ycbcrImage.YChannel[yo + x]; + byte green = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte blue = this.ycbcrImage.CrChannel[co + (x / scale)]; + + TColor packed = default(TColor); + packed.PackFromBytes(red, green, blue, 255); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); @@ -1443,8 +1299,7 @@ namespace ImageSharp.Formats /// The packed format. uint, long, float. /// The image to assign the resolution to. private void AssignResolution(Image image) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) { @@ -1453,6 +1308,11 @@ namespace ImageSharp.Formats } } + struct StackallocUnzigData + { + internal fixed int Data [64]; + } + /// /// Processes the SOS (Start of scan marker). /// @@ -1479,7 +1339,8 @@ namespace ImageSharp.Formats this.ReadFull(this.temp, 0, remaining); byte scanComponentCount = this.temp[0]; - if (remaining != 4 + (2 * scanComponentCount)) + int scanComponentCountX2 = 2 * scanComponentCount; + if (remaining != 4 + scanComponentCountX2) { throw new ImageFormatException("SOS length inconsistent with number of components"); } @@ -1489,51 +1350,7 @@ namespace ImageSharp.Formats for (int i = 0; i < scanComponentCount; i++) { - // Component selector. - int cs = this.temp[1 + (2 * i)]; - int compIndex = -1; - for (int j = 0; j < this.componentCount; j++) - { - Component compv = this.componentArray[j]; - if (cs == compv.Identifier) - { - compIndex = j; - } - } - - if (compIndex < 0) - { - throw new ImageFormatException("Unknown component selector"); - } - - scan[i].Index = (byte)compIndex; - - // Section B.2.3 states that "the value of Cs_j shall be different from - // the values of Cs_1 through Cs_(j-1)". Since we have previously - // verified that a frame's component identifiers (C_i values in section - // B.2.2) are unique, it suffices to check that the implicit indexes - // into comp are unique. - for (int j = 0; j < i; j++) - { - if (scan[i].Index == scan[j].Index) - { - throw new ImageFormatException("Repeated component selector"); - } - } - - totalHv += this.componentArray[compIndex].HorizontalFactor * this.componentArray[compIndex].VerticalFactor; - - scan[i].DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); - if (scan[i].DcTableSelector > MaxTh) - { - throw new ImageFormatException("Bad DC table selector value"); - } - - scan[i].AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); - if (scan[i].AcTableSelector > MaxTh) - { - throw new ImageFormatException("Bad AC table selector value"); - } + this.ProcessScanImpl(i, ref scan[i], scan, ref totalHv); } // Section B.2.3 states that if there is more than one component then the @@ -1558,18 +1375,18 @@ namespace ImageSharp.Formats // significant bit. // For baseline JPEGs, these parameters are hard-coded to 0/63/0/0. int zigStart = 0; - int zigEnd = Block.BlockSize - 1; + int zigEnd = BlockF.BlockSize - 1; int ah = 0; int al = 0; if (this.isProgressive) { - zigStart = this.temp[1 + (2 * scanComponentCount)]; - zigEnd = this.temp[2 + (2 * scanComponentCount)]; - ah = this.temp[3 + (2 * scanComponentCount)] >> 4; - al = this.temp[3 + (2 * scanComponentCount)] & 0x0f; + zigStart = this.temp[1 + scanComponentCountX2]; + zigEnd = this.temp[2 + scanComponentCountX2]; + ah = this.temp[3 + scanComponentCountX2] >> 4; + al = this.temp[3 + scanComponentCountX2] & 0x0f; - if ((zigStart == 0 && zigEnd != 0) || zigStart > zigEnd || Block.BlockSize <= zigEnd) + if ((zigStart == 0 && zigEnd != 0) || zigStart > zigEnd || BlockF.BlockSize <= zigEnd) { throw new ImageFormatException("Bad spectral selection bounds"); } @@ -1603,12 +1420,10 @@ namespace ImageSharp.Formats int compIndex = scan[i].Index; if (this.progCoeffs[compIndex] == null) { - this.progCoeffs[compIndex] = new Block[mxx * myy * this.componentArray[compIndex].HorizontalFactor * this.componentArray[compIndex].VerticalFactor]; + var size = mxx * myy * this.componentArray[compIndex].HorizontalFactor + * this.componentArray[compIndex].VerticalFactor; - for (int j = 0; j < this.progCoeffs[compIndex].Length; j++) - { - this.progCoeffs[compIndex][j] = new Block(); - } + this.progCoeffs[compIndex] = new Block8x8F[size]; } } } @@ -1619,13 +1434,22 @@ namespace ImageSharp.Formats byte expectedRst = JpegConstants.Markers.RST0; // b is the decoded coefficients block, in natural (not zig-zag) order. - Block b; + //Block b; int[] dc = new int[MaxComponents]; // bx and by are the location of the current block, in units of 8x8 // blocks: the third block in the first row has (bx, by) = (2, 0). int bx, by, blockCount = 0; + Block8x8F b = new Block8x8F(); + Block8x8F temp1 = new Block8x8F(); + Block8x8F temp2 = new Block8x8F(); + + // Tricky way to copy contents of the Unzig static variable to the stack: + StackallocUnzigData unzigOnStack = new StackallocUnzigData(); + int* unzigPtr = unzigOnStack.Data; + Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, 64); + for (int my = 0; my < myy; my++) { for (int mx = 0; mx < mxx; mx++) @@ -1635,7 +1459,6 @@ namespace ImageSharp.Formats int compIndex = scan[i].Index; int hi = this.componentArray[compIndex].HorizontalFactor; int vi = this.componentArray[compIndex].VerticalFactor; - Block qt = this.quantizationTables[this.componentArray[compIndex].Selector]; for (int j = 0; j < hi * vi; j++) { @@ -1678,167 +1501,60 @@ namespace ImageSharp.Formats } } - // Load the previous partially decoded coefficients, if applicable. - b = this.isProgressive ? this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] : new Block(); + var qtIndex = this.componentArray[compIndex].Selector; - if (ah != 0) - { - this.Refine(b, this.huffmanTrees[AcTable, scan[i].AcTableSelector], zigStart, zigEnd, 1 << al); - } - else + // TODO: Find a way to clean up this mess + + fixed (Block8x8F* qtp = &this.quantizationTables[qtIndex]) { - int zig = zigStart; - if (zig == 0) + if (this.isProgressive) + // Load the previous partially decoded coefficients, if applicable. { - zig++; + this.blockIndex = ((@by * mxx) * hi) + bx; - // Decode the DC coefficient, as specified in section F.2.2.1. - byte value = this.DecodeHuffman(this.huffmanTrees[DcTable, scan[i].DcTableSelector]); - if (value > 16) + fixed (Block8x8F* bp = &this.progCoeffs[compIndex][this.blockIndex]) { - throw new ImageFormatException("Excessive DC component"); + this.ProcessBlockImpl( + ah, + bp, + &temp1, + &temp2, + unzigPtr, + scan, + i, + zigStart, + zigEnd, + al, + dc, + compIndex, + @by, + mxx, + hi, + bx, + qtp); } - - int deltaDC = this.ReceiveExtend(value); - dc[compIndex] += deltaDC; - b[0] = dc[compIndex] << al; - } - - if (zig <= zigEnd && this.eobRun > 0) - { - this.eobRun--; } else { - // Decode the AC coefficients, as specified in section F.2.2.2. - Huffman huffv = this.huffmanTrees[AcTable, scan[i].AcTableSelector]; - for (; zig <= zigEnd; zig++) - { - byte value = this.DecodeHuffman(huffv); - byte val0 = (byte)(value >> 4); - byte val1 = (byte)(value & 0x0f); - if (val1 != 0) - { - zig += val0; - if (zig > zigEnd) - { - break; - } - - int ac = this.ReceiveExtend(val1); - b[Unzig[zig]] = ac << al; - } - else - { - if (val0 != 0x0f) - { - this.eobRun = (ushort)(1 << val0); - if (val0 != 0) - { - this.eobRun |= (ushort)this.DecodeBits(val0); - } - - this.eobRun--; - break; - } - - zig += 0x0f; - } - } - } - } - - if (this.isProgressive) - { - if (zigEnd != Block.BlockSize - 1 || al != 0) - { - // We haven't completely decoded this 8x8 block. Save the coefficients. - this.progCoeffs[compIndex][((by * mxx) * hi) + bx] = b; - - // At this point, we could execute the rest of the loop body to dequantize and - // perform the inverse DCT, to save early stages of a progressive image to the - // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, - // the jpeg.Decode function does not return until the entire image is decoded, - // so we "continue" here to avoid wasted computation. - continue; - } - } - - // Dequantize, perform the inverse DCT and store the block to the image. - for (int zig = 0; zig < Block.BlockSize; zig++) - { - b[Unzig[zig]] *= qt[zig]; - } - - IDCT.Transform(b); - - byte[] dst; - int offset; - int stride; - - if (this.componentCount == 1) - { - dst = this.grayImage.Pixels; - stride = this.grayImage.Stride; - offset = this.grayImage.Offset + (8 * ((by * this.grayImage.Stride) + bx)); - } - else - { - switch (compIndex) - { - case 0: - dst = this.ycbcrImage.YChannel; - stride = this.ycbcrImage.YStride; - offset = this.ycbcrImage.YOffset + (8 * ((by * this.ycbcrImage.YStride) + bx)); - break; - - case 1: - dst = this.ycbcrImage.CbChannel; - stride = this.ycbcrImage.CStride; - offset = this.ycbcrImage.COffset + (8 * ((by * this.ycbcrImage.CStride) + bx)); - break; - - case 2: - dst = this.ycbcrImage.CrChannel; - stride = this.ycbcrImage.CStride; - offset = this.ycbcrImage.COffset + (8 * ((by * this.ycbcrImage.CStride) + bx)); - break; - - case 3: - - dst = this.blackPixels; - stride = this.blackStride; - offset = 8 * ((by * this.blackStride) + bx); - break; - - default: - throw new ImageFormatException("Too many components"); - } - } - - // Level shift by +128, clip to [0, 255], and write to dst. - for (int y = 0; y < 8; y++) - { - int y8 = y * 8; - int yStride = y * stride; - - for (int x = 0; x < 8; x++) - { - int c = b[y8 + x]; - if (c < -128) - { - c = 0; - } - else if (c > 127) - { - c = 255; - } - else - { - c += 128; - } - - dst[yStride + x + offset] = (byte)c; + b.Clear(); + this.ProcessBlockImpl( + ah, + &b, + &temp1, + &temp2, + unzigPtr, + scan, + i, + zigStart, + zigEnd, + al, + dc, + compIndex, + @by, + mxx, + hi, + bx, + qtp); } } } @@ -1882,15 +1598,241 @@ namespace ImageSharp.Formats // for my } + private void ProcessBlockImpl( + int ah, + Block8x8F* b, + Block8x8F* temp1, + Block8x8F* temp2, + int* unzigPtr, + Scan[] scan, + int i, + int zigStart, + int zigEnd, + int al, + int[] dc, + int compIndex, + int @by, + int mxx, + int hi, + int bx, + Block8x8F* qt) + { + var huffmannIdx = AcTable * ThRowSize + scan[i].AcTableSelector; + if (ah != 0) + { + this.Refine(b, ref this.huffmanTrees[huffmannIdx], unzigPtr, zigStart, zigEnd, 1 << al); + } + else + { + int zig = zigStart; + if (zig == 0) + { + zig++; + + // Decode the DC coefficient, as specified in section F.2.2.1. + byte value = this.DecodeHuffman( + ref this.huffmanTrees[DcTable * ThRowSize + scan[i].DcTableSelector]); + if (value > 16) + { + throw new ImageFormatException("Excessive DC component"); + } + + int deltaDC = this.bits.ReceiveExtend(value, this); + dc[compIndex] += deltaDC; + + // b[0] = dc[compIndex] << al; + Block8x8F.SetScalarAt(b, 0, dc[compIndex] << al); + } + + if (zig <= zigEnd && this.eobRun > 0) + { + this.eobRun--; + } + else + { + // Decode the AC coefficients, as specified in section F.2.2.2. + // Huffman huffv = ; + for (; zig <= zigEnd; zig++) + { + byte value = this.DecodeHuffman(ref this.huffmanTrees[huffmannIdx]); + byte val0 = (byte)(value >> 4); + byte val1 = (byte)(value & 0x0f); + if (val1 != 0) + { + zig += val0; + if (zig > zigEnd) + { + break; + } + + int ac = this.bits.ReceiveExtend(val1, this); + + // b[Unzig[zig]] = ac << al; + Block8x8F.SetScalarAt(b, unzigPtr[zig], ac << al); + } + else + { + if (val0 != 0x0f) + { + this.eobRun = (ushort)(1 << val0); + if (val0 != 0) + { + this.eobRun |= (ushort)this.DecodeBits(val0); + } + + this.eobRun--; + break; + } + + zig += 0x0f; + } + } + } + } + + if (this.isProgressive) + { + if (zigEnd != BlockF.BlockSize - 1 || al != 0) + { + // We haven't completely decoded this 8x8 block. Save the coefficients. + + // TODO!!! + //throw new NotImplementedException(); + //this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] = b.Clone(); + this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] = *b; + + // At this point, we could execute the rest of the loop body to dequantize and + // perform the inverse DCT, to save early stages of a progressive image to the + // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, + // the jpeg.Decode function does not return until the entire image is decoded, + // so we "continue" here to avoid wasted computation. + return; + } + } + + // Dequantize, perform the inverse DCT and store the block to the image. + Block8x8F.UnZig(b, qt, unzigPtr); + + b->TransformIDCTInto(ref *temp1, ref *temp2); + + byte[] dst; + int offset; + int stride; + + if (this.componentCount == 1) + { + dst = this.grayImage.Pixels; + stride = this.grayImage.Stride; + offset = this.grayImage.Offset + (8 * ((@by * this.grayImage.Stride) + bx)); + } + else + { + switch (compIndex) + { + case 0: + dst = this.ycbcrImage.YChannel; + stride = this.ycbcrImage.YStride; + offset = this.ycbcrImage.YOffset + (8 * ((@by * this.ycbcrImage.YStride) + bx)); + break; + + case 1: + dst = this.ycbcrImage.CbChannel; + stride = this.ycbcrImage.CStride; + offset = this.ycbcrImage.COffset + (8 * ((@by * this.ycbcrImage.CStride) + bx)); + break; + + case 2: + dst = this.ycbcrImage.CrChannel; + stride = this.ycbcrImage.CStride; + offset = this.ycbcrImage.COffset + (8 * ((@by * this.ycbcrImage.CStride) + bx)); + break; + + case 3: + + dst = this.blackPixels; + stride = this.blackStride; + offset = 8 * ((@by * this.blackStride) + bx); + break; + + default: + throw new ImageFormatException("Too many components"); + } + } + + // Level shift by +128, clip to [0, 255], and write to dst. + + temp1->CopyColorsTo(new MutableSpan(dst, offset), stride, temp2); + } + + private void ProcessScanImpl(int i, ref Scan currentScan, Scan[] scan, ref int totalHv) + { + // Component selector. + int cs = this.temp[1 + (2 * i)]; + int compIndex = -1; + for (int j = 0; j < this.componentCount; j++) + { + //Component compv = ; + if (cs == this.componentArray[j].Identifier) + { + compIndex = j; + } + } + + if (compIndex < 0) + { + throw new ImageFormatException("Unknown component selector"); + } + + currentScan.Index = (byte)compIndex; + + this.ProcessComponentImpl(i, ref currentScan, scan, ref totalHv, ref this.componentArray[compIndex]); + } + + private void ProcessComponentImpl( + int i, + ref Scan currentScan, + Scan[] scan, + ref int totalHv, + ref Component currentComponent) + { + // Section B.2.3 states that "the value of Cs_j shall be different from + // the values of Cs_1 through Cs_(j-1)". Since we have previously + // verified that a frame's component identifiers (C_i values in section + // B.2.2) are unique, it suffices to check that the implicit indexes + // into comp are unique. + for (int j = 0; j < i; j++) + { + if (currentScan.Index == scan[j].Index) + { + throw new ImageFormatException("Repeated component selector"); + } + } + + totalHv += currentComponent.HorizontalFactor * currentComponent.VerticalFactor; + + currentScan.DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); + if (currentScan.DcTableSelector > MaxTh) + { + throw new ImageFormatException("Bad DC table selector value"); + } + + currentScan.AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); + if (currentScan.AcTableSelector > MaxTh) + { + throw new ImageFormatException("Bad AC table selector value"); + } + } + /// /// Decodes a successive approximation refinement block, as specified in section G.1.2. /// /// The block of coefficients /// The Huffman tree + /// /// The zig-zag start index /// The zig-zag end index /// The low transform offset - private void Refine(Block b, Huffman h, int zigStart, int zigEnd, int delta) + private void Refine(Block8x8F* b, ref Huffman h, int* unzigPtr, int zigStart, int zigEnd, int delta) { // Refining a DC component is trivial. if (zigStart == 0) @@ -1903,7 +1845,12 @@ namespace ImageSharp.Formats bool bit = this.DecodeBit(); if (bit) { - b[0] |= delta; + int stuff = (int)Block8x8F.GetScalarAt(b, 0); + + // int stuff = (int)b[0]; + stuff |= delta; + // b[0] = stuff; + Block8x8F.SetScalarAt(b, 0, stuff); } return; @@ -1917,7 +1864,7 @@ namespace ImageSharp.Formats { bool done = false; int z = 0; - byte val = this.DecodeHuffman(h); + byte val = this.DecodeHuffman(ref h); int val0 = val >> 4; int val1 = val & 0x0f; @@ -1954,6 +1901,8 @@ namespace ImageSharp.Formats break; } + int blah = zig; + zig = this.RefineNonZeroes(b, zig, zigEnd, val0, delta); if (zig > zigEnd) { @@ -1962,7 +1911,8 @@ namespace ImageSharp.Formats if (z != 0) { - b[Unzig[zig]] = z; + //b[Unzig[zig]] = z; + Block8x8F.SetScalarAt(b, unzigPtr[zig], z); } } } @@ -1984,12 +1934,15 @@ namespace ImageSharp.Formats /// The non-zero entry /// The low transform offset /// The - private int RefineNonZeroes(Block b, int zig, int zigEnd, int nz, int delta) + private int RefineNonZeroes(Block8x8F* b, int zig, int zigEnd, int nz, int delta) { for (; zig <= zigEnd; zig++) { int u = Unzig[zig]; - if (b[u] == 0) + float bu = Block8x8F.GetScalarAt(b, u); + + // TODO: Are the equality comparsions OK with floating point values? Isn't an epsilon value necessary? + if (bu == 0) { if (nz == 0) { @@ -2006,13 +1959,15 @@ namespace ImageSharp.Formats continue; } - if (b[u] >= 0) + if (bu >= 0) { - b[u] += delta; + //b[u] += delta; + Block8x8F.SetScalarAt(b, u, bu + delta); } else { - b[u] -= delta; + //b[u] -= delta; + Block8x8F.SetScalarAt(b, u, bu - delta); } } @@ -2094,7 +2049,8 @@ namespace ImageSharp.Formats return true; } - return this.componentArray[0].Identifier == 'R' && this.componentArray[1].Identifier == 'G' && this.componentArray[2].Identifier == 'B'; + return this.componentArray[0].Identifier == 'R' && this.componentArray[1].Identifier == 'G' + && this.componentArray[2].Identifier == 'B'; } /// @@ -2107,9 +2063,9 @@ namespace ImageSharp.Formats /// The y luminance component. /// The cb chroma component. /// The cr chroma component. - private void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) - where TColor : struct, IPackedPixel - where TPacked : struct + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PackYcbCr(ref TColor packed, byte y, byte cb, byte cr) + where TColor : struct, IPackedPixel where TPacked : struct { int ccb = cb - 128; int ccr = cr - 128; @@ -2134,8 +2090,7 @@ namespace ImageSharp.Formats /// The x-position within the image. /// The y-position within the image. private void PackCmyk(ref TColor packed, byte y, byte cb, byte cr, int xx, int yy) - where TColor : struct, IPackedPixel - where TPacked : struct + where TColor : struct, IPackedPixel where TPacked : struct { // TODO: We can speed this up further with Vector4 int ccb = cb - 128; @@ -2178,10 +2133,21 @@ namespace ImageSharp.Formats public byte AcTableSelector { get; set; } } + /// + /// ReadByteStuffedByte was throwing exceptions on normal execution path (very inefficent) + /// It's better tho have an error code for this! + /// + internal enum ErrorCodes + { + NoError, + // ReSharper disable once InconsistentNaming + MissingFF00 + } + /// /// The missing ff00 exception. /// - private class MissingFF00Exception : Exception + internal class MissingFF00Exception : Exception { } @@ -2196,8 +2162,17 @@ namespace ImageSharp.Formats /// The EOF (End of File exception). /// Thrown when the decoder encounters an EOF marker without a proceeding EOI (End Of Image) marker /// - private class EOFException : Exception + internal class EOFException : Exception + { + } + + public void Dispose() { + for (int i = 0; i < this.huffmanTrees.Length; i++) + { + this.huffmanTrees[i].Dispose(); + } + this.bytes.Dispose(); } } } diff --git a/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs index 82a99010f2..66d7cdc7b6 100644 --- a/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegEncoderCore.cs @@ -246,6 +246,7 @@ namespace ImageSharp.Formats /// /// The AC luminance huffman table index /// + LuminanceAC = 1, // ReSharper restore UnusedMember.Local @@ -487,9 +488,9 @@ namespace ImageSharp.Formats /// The quantization table index. /// The previous DC value. /// The - private int WriteBlock(Block block, QuantIndex index, int prevDC) + private int WriteBlock(ref Block block, QuantIndex index, int prevDC) { - FDCT.Transform(block); + FDCT.Transform(ref block); // Emit the DC delta. int dc = Round(block[0], 8 * this.quant[(int)index][0]); @@ -540,7 +541,7 @@ namespace ImageSharp.Formats /// The red chroma block. /// The blue chroma block. // ReSharper disable StyleCop.SA1305 - private void ToYCbCr(PixelAccessor pixels, int x, int y, Block yBlock, Block cbBlock, Block crBlock) + private void ToYCbCr(PixelAccessor pixels, int x, int y, ref Block yBlock, ref Block cbBlock, ref Block crBlock) // ReSharper restore StyleCop.SA1305 where TColor : struct, IPackedPixel where TPacked : struct @@ -577,7 +578,7 @@ namespace ImageSharp.Formats /// /// The destination block array /// The source block array. - private void Scale16X16To8X8(Block destination, Block[] source) + private void Scale16X16To8X8(ref Block destination, Block[] source) { for (int i = 0; i < 4; i++) { @@ -847,10 +848,10 @@ namespace ImageSharp.Formats where TColor : struct, IPackedPixel where TPacked : struct { - Block b = new Block(); - Block cb = new Block(); - Block cr = new Block(); - + Block b = Block.Create(); + Block cb = Block.Create(); + Block cr = Block.Create(); + // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; @@ -858,12 +859,16 @@ namespace ImageSharp.Formats { for (int x = 0; x < pixels.Width; x += 8) { - this.ToYCbCr(pixels, x, y, b, cb, cr); - prevDCY = this.WriteBlock(b, QuantIndex.Luminance, prevDCY); - prevDCCb = this.WriteBlock(cb, QuantIndex.Chrominance, prevDCCb); - prevDCCr = this.WriteBlock(cr, QuantIndex.Chrominance, prevDCCr); + this.ToYCbCr(pixels, x, y, ref b, ref cb, ref cr); + prevDCY = this.WriteBlock(ref b, QuantIndex.Luminance, prevDCY); + prevDCCb = this.WriteBlock(ref cb, QuantIndex.Chrominance, prevDCCb); + prevDCCr = this.WriteBlock(ref cr, QuantIndex.Chrominance, prevDCCr); } } + + b.Dispose(); + cb.Dispose(); + cr.Dispose(); } /// @@ -877,23 +882,13 @@ namespace ImageSharp.Formats where TColor : struct, IPackedPixel where TPacked : struct { - Block b = new Block(); - Block[] cb = new Block[4]; - Block[] cr = new Block[4]; - + Block b = Block.Create(); + Block[] cb = Block.CreateArray(4); + Block[] cr = Block.CreateArray(4); + // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; - for (int i = 0; i < 4; i++) - { - cb[i] = new Block(); - } - - for (int i = 0; i < 4; i++) - { - cr[i] = new Block(); - } - for (int y = 0; y < pixels.Height; y += 16) { for (int x = 0; x < pixels.Width; x += 16) @@ -903,16 +898,20 @@ namespace ImageSharp.Formats int xOff = (i & 1) * 8; int yOff = (i & 2) * 4; - this.ToYCbCr(pixels, x + xOff, y + yOff, b, cb[i], cr[i]); - prevDCY = this.WriteBlock(b, QuantIndex.Luminance, prevDCY); + this.ToYCbCr(pixels, x + xOff, y + yOff, ref b, ref cb[i], ref cr[i]); + prevDCY = this.WriteBlock(ref b, QuantIndex.Luminance, prevDCY); } - this.Scale16X16To8X8(b, cb); - prevDCCb = this.WriteBlock(b, QuantIndex.Chrominance, prevDCCb); - this.Scale16X16To8X8(b, cr); - prevDCCr = this.WriteBlock(b, QuantIndex.Chrominance, prevDCCr); + this.Scale16X16To8X8(ref b, cb); + prevDCCb = this.WriteBlock(ref b, QuantIndex.Chrominance, prevDCCb); + this.Scale16X16To8X8(ref b, cr); + prevDCCr = this.WriteBlock(ref b, QuantIndex.Chrominance, prevDCCr); } } + + b.Dispose(); + Block.DisposeAll(cb); + Block.DisposeAll(cr); } /// diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs index 549ac05ef4..c91b0ad1b1 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BitmapTests.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // +using ImageSharp.Formats; + namespace ImageSharp.Tests { using System.IO; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs new file mode 100644 index 0000000000..524b84082e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -0,0 +1,367 @@ +// Uncomment this to turn unit tests into benchmarks: +//#define BENCHMARKING + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using ImageSharp.Formats; +using Xunit; +using Xunit.Abstractions; +// ReSharper disable InconsistentNaming + +namespace ImageSharp.Tests.Formats.Jpg +{ + public class Block8x8FTests : UtilityTestClassBase + { +#if BENCHMARKING + public const int Times = 1000000; +#else + public const int Times = 1; +#endif + + public Block8x8FTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Indexer() + { + float sum = 0; + this.Measure(Times, () => + { + Block8x8F block = new Block8x8F(); + + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + block[i] = i; + } + sum = 0; + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + sum += block[i]; + } + }); + Assert.Equal(sum, 64f * 63f * 0.5f); + } + + [Fact] + public unsafe void Indexer_GetScalarAt_SetScalarAt() + { + float sum = 0; + Measure(Times, () => + { + Block8x8F block = new Block8x8F(); + + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + Block8x8F.SetScalarAt(&block, i, i); + } + sum = 0; + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + sum += Block8x8F.GetScalarAt(&block, i); + } + }); + Assert.Equal(sum, 64f*63f*0.5f); + } + + [Fact] + public void Indexer_ReferenceBenchmarkWithArray() + { + float sum = 0; + + + Measure(Times, () => + { + //Block8x8F block = new Block8x8F(); + float[] block = new float[64]; + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + block[i] = i; + } + sum = 0; + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + sum += block[i]; + } + }); + Assert.Equal(sum, 64f*63f*0.5f); + } + + [Fact] + public void Load_Store_FloatArray() + { + float[] data = new float[Block8x8F.ScalarCount]; + float[] mirror = new float[Block8x8F.ScalarCount]; + + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + data[i] = i; + } + Measure(Times, () => + { + Block8x8F b = new Block8x8F(); + b.LoadFrom(data); + b.CopyTo(mirror); + }); + + Assert.Equal(data, mirror); + //PrintLinearData((MutableSpan)mirror); + } + + [Fact] + public unsafe void Load_Store_FloatArray_Ptr() + { + float[] data = new float[Block8x8F.ScalarCount]; + float[] mirror = new float[Block8x8F.ScalarCount]; + + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + data[i] = i; + } + Measure(Times, () => + { + Block8x8F b = new Block8x8F(); + Block8x8F.LoadFrom(&b, data); + Block8x8F.CopyTo(&b, mirror); + }); + + Assert.Equal(data, mirror); + //PrintLinearData((MutableSpan)mirror); + } + + [Fact] + public void Load_Store_IntArray() + { + int[] data = new int[Block8x8F.ScalarCount]; + int[] mirror = new int[Block8x8F.ScalarCount]; + + for (int i = 0; i < Block8x8F.ScalarCount; i++) + { + data[i] = i; + } + Measure(Times, () => + { + Block8x8F v = new Block8x8F(); + v.LoadFrom(data); + v.CopyTo(mirror); + }); + + + Assert.Equal(data, mirror); + //PrintLinearData((MutableSpan)mirror); + } + + + [Fact] + public void TransposeInto() + { + float[] expected = Create8x8FloatData(); + ReferenceImplementations.Transpose8x8(expected); + + Block8x8F source = new Block8x8F(); + source.LoadFrom(Create8x8FloatData()); + + Block8x8F dest = new Block8x8F(); + source.TransposeInto(ref dest); + + float[] actual = new float[64]; + dest.CopyTo(actual); + + Assert.Equal(expected, actual); + } + + + + private class BufferHolder + { + public Block8x8F Buffer; + } + + [Fact] + public void TranposeInto_Benchmark() + { + BufferHolder source = new BufferHolder(); + source.Buffer.LoadFrom(Create8x8FloatData()); + BufferHolder dest = new BufferHolder(); + + Output.WriteLine($"TranposeInto_PinningImpl_Benchmark X {Times} ..."); + Stopwatch sw = Stopwatch.StartNew(); + + for (int i = 0; i < Times; i++) + { + source.Buffer.TransposeInto(ref dest.Buffer); + } + + sw.Stop(); + Output.WriteLine($"TranposeInto_PinningImpl_Benchmark finished in {sw.ElapsedMilliseconds} ms"); + + } + + [Fact] + public void iDCT2D8x4_LeftPart() + { + float[] sourceArray = Create8x8FloatData(); + float[] expectedDestArray = new float[64]; + + ReferenceImplementations.iDCT2D8x4_32f(sourceArray, expectedDestArray); + + Block8x8F source = new Block8x8F(); + source.LoadFrom(sourceArray); + + Block8x8F dest = new Block8x8F(); + + source.IDCT8x4_LeftPart(ref dest); + + float[] actualDestArray = new float[64]; + dest.CopyTo(actualDestArray); + + Print8x8Data(expectedDestArray); + Output.WriteLine("**************"); + Print8x8Data(actualDestArray); + + Assert.Equal(expectedDestArray, actualDestArray); + } + + [Fact] + public void iDCT2D8x4_RightPart() + { + MutableSpan sourceArray = Create8x8FloatData(); + MutableSpan expectedDestArray = new float[64]; + + ReferenceImplementations.iDCT2D8x4_32f(sourceArray.Slice(4), expectedDestArray.Slice(4)); + + Block8x8F source = new Block8x8F(); + source.LoadFrom(sourceArray); + + Block8x8F dest = new Block8x8F(); + + source.IDCT8x4_RightPart(ref dest); + + float[] actualDestArray = new float[64]; + dest.CopyTo(actualDestArray); + + Print8x8Data(expectedDestArray); + Output.WriteLine("**************"); + Print8x8Data(actualDestArray); + + Assert.Equal(expectedDestArray.Data, actualDestArray); + } + + private struct ApproximateFloatComparer : IEqualityComparer + { + private const float Eps = 0.0001f; + + public bool Equals(float x, float y) + { + float d = x - y; + + return d > -Eps && d < Eps; + } + + public int GetHashCode(float obj) + { + throw new InvalidOperationException(); + } + } + + [Fact] + public void IDCTInto() + { + float[] sourceArray = Create8x8FloatData(); + float[] expectedDestArray = new float[64]; + float[] tempArray = new float[64]; + + ReferenceImplementations.iDCT2D_llm(sourceArray, expectedDestArray, tempArray); + + //ReferenceImplementations.iDCT8x8_llm_sse(sourceArray, expectedDestArray, tempArray); + + Block8x8F source = new Block8x8F(); + source.LoadFrom(sourceArray); + + Block8x8F dest = new Block8x8F(); + Block8x8F tempBuffer = new Block8x8F(); + + source.TransformIDCTInto(ref dest, ref tempBuffer); + + float[] actualDestArray = new float[64]; + dest.CopyTo(actualDestArray); + + Print8x8Data(expectedDestArray); + Output.WriteLine("**************"); + Print8x8Data(actualDestArray); + Assert.Equal(expectedDestArray, actualDestArray, new ApproximateFloatComparer()); + Assert.Equal(expectedDestArray, actualDestArray, new ApproximateFloatComparer()); + } + + + [Fact] + public unsafe void CopyColorsTo() + { + var data = Create8x8FloatData(); + Block8x8F block = new Block8x8F(); + block.LoadFrom(data); + block.MultiplyAllInplace(new Vector4(5, 5, 5, 5)); + + int stride = 256; + int height = 42; + int offset = height*10 + 20; + + byte[] colorsExpected = new byte[stride*height]; + byte[] colorsActual = new byte[stride*height]; + + Block8x8F temp = new Block8x8F(); + + ReferenceImplementations.CopyColorsTo(ref block, new MutableSpan(colorsExpected, offset), stride); + + block.CopyColorsTo(new MutableSpan(colorsActual, offset), stride, &temp); + + //Output.WriteLine("******* EXPECTED: *********"); + //PrintLinearData(colorsExpected); + //Output.WriteLine("******** ACTUAL: **********"); + + Assert.Equal(colorsExpected, colorsActual); + } + + private static float[] Create8x8ColorCropTestData() + { + float[] result = new float[64]; + for (int i = 0; i < 8; i++) + { + for (int j = 0; j < 8; j++) + { + result[i * 8 + j] = -300 + i * 100 + j * 10; + } + } + return result; + } + + [Fact] + public void TransformByteConvetibleColorValuesInto() + { + Block8x8F block = new Block8x8F(); + var input = Create8x8ColorCropTestData(); + block.LoadFrom(input); + Output.WriteLine("Input:"); + PrintLinearData(input); + + + Block8x8F dest = new Block8x8F(); + block.TransformByteConvetibleColorValuesInto(ref dest); + + float[] array = new float[64]; + dest.CopyTo(array); + Output.WriteLine("Result:"); + PrintLinearData(array); + foreach (float val in array) + { + Assert.InRange(val, 0, 255); + } + } + + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs new file mode 100644 index 0000000000..1177b57aba --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ImageSharp.Formats; +using Xunit; +using Xunit.Abstractions; + +namespace ImageSharp.Tests.Formats.Jpg +{ + public class JpegTests + { + + public const string TestOutputDirectory = "TestOutput/Jpeg"; + + private ITestOutputHelper Output { get; } + + public JpegTests(ITestOutputHelper output) + { + Output = output; + } + + protected string CreateTestOutputFile(string fileName) + { + if (!Directory.Exists(TestOutputDirectory)) + { + Directory.CreateDirectory(TestOutputDirectory); + } + + //string id = Guid.NewGuid().ToString().Substring(0, 4); + + string ext = Path.GetExtension(fileName); + fileName = Path.GetFileNameWithoutExtension(fileName); + + return $"{TestOutputDirectory}/{fileName}{ext}"; + } + + protected Stream CreateOutputStream(string fileName) + { + fileName = CreateTestOutputFile(fileName); + Output?.WriteLine("Opened for write: "+fileName); + return File.OpenWrite(fileName); + } + + public static IEnumerable AllJpegFiles + => TestImages.Jpeg.All.Select(fn => new object[] {fn}); + + [Theory] + [MemberData(nameof(AllJpegFiles))] + public void OpenJpeg_SaveBmp(string jpegPath) + { + string bmpFileName = Path.GetFileNameWithoutExtension(jpegPath) + ".bmp"; + + using (var inputStream = File.OpenRead(jpegPath)) + { + var image = new Image(inputStream); + + using (var outputStream = CreateOutputStream(bmpFileName)) + { + image.Save(outputStream, new BmpFormat()); + } + } + } + + public static IEnumerable AllBmpFiles + => TestImages.Bmp.All.Select(fn => new object[] {fn}); + + [Theory] + [MemberData(nameof(AllBmpFiles))] + public void OpenBmp_SaveJpeg(string bmpPath) + { + string jpegPath = Path.GetFileNameWithoutExtension(bmpPath) + ".jpeg"; + + using (var inputStream = File.OpenRead(bmpPath)) + { + var image = new Image(inputStream); + + using (var outputStream = CreateOutputStream(jpegPath)) + { + image.Save(outputStream, new JpegFormat()); + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs new file mode 100644 index 0000000000..b620c1d19f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementations.cs @@ -0,0 +1,335 @@ +// ReSharper disable InconsistentNaming + +namespace ImageSharp.Tests.Formats.Jpg +{ + using System.Numerics; + using System.Runtime.CompilerServices; + + using ImageSharp.Formats; + + /// + /// This class contains simplified (unefficient) reference implementations to produce verification data for unit tests + /// DCT code Ported from https://github.com/norishigefukushima/dct_simd + /// + internal static class ReferenceImplementations + { + /// + /// Transpose 8x8 block stored linearly in a span (inplace) + /// + /// + internal static void Transpose8x8(MutableSpan data) + { + for (int i = 1; i < 8; i++) + { + int i8 = i * 8; + for (int j = 0; j < i; j++) + { + float tmp = data[i8 + j]; + data[i8 + j] = data[j * 8 + i]; + data[j * 8 + i] = tmp; + } + } + } + + /// + /// Transpose 8x8 block stored linearly in a span + /// + internal static void Transpose8x8(MutableSpan src, MutableSpan dest) + { + for (int i = 0; i < 8; i++) + { + int i8 = i * 8; + for (int j = 0; j < 8; j++) + { + dest[j * 8 + i] = src[i8 + j]; + } + } + } + + /// + /// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L200 + /// + /// + /// + private static void iDCT1Dllm_32f(MutableSpan y, MutableSpan x) + { + float a0, a1, a2, a3, b0, b1, b2, b3; + float z0, z1, z2, z3, z4; + + float r0 = 1.414214f; + float r1 = 1.387040f; + float r2 = 1.306563f; + float r3 = 1.175876f; + float r4 = 1.000000f; + float r5 = 0.785695f; + float r6 = 0.541196f; + float r7 = 0.275899f; + + z0 = y[1] + y[7]; + z1 = y[3] + y[5]; + z2 = y[3] + y[7]; + z3 = y[1] + y[5]; + z4 = (z0 + z1) * r3; + + z0 = z0 * (-r3 + r7); + z1 = z1 * (-r3 - r1); + z2 = z2 * (-r3 - r5) + z4; + z3 = z3 * (-r3 + r5) + z4; + + b3 = y[7] * (-r1 + r3 + r5 - r7) + z0 + z2; + b2 = y[5] * (r1 + r3 - r5 + r7) + z1 + z3; + b1 = y[3] * (r1 + r3 + r5 - r7) + z1 + z2; + b0 = y[1] * (r1 + r3 - r5 - r7) + z0 + z3; + + z4 = (y[2] + y[6]) * r6; + z0 = y[0] + y[4]; + z1 = y[0] - y[4]; + z2 = z4 - y[6] * (r2 + r6); + z3 = z4 + y[2] * (r2 - r6); + a0 = z0 + z3; + a3 = z0 - z3; + a1 = z1 + z2; + a2 = z1 - z2; + + x[0] = a0 + b0; + x[7] = a0 - b0; + x[1] = a1 + b1; + x[6] = a1 - b1; + x[2] = a2 + b2; + x[5] = a2 - b2; + x[3] = a3 + b3; + x[4] = a3 - b3; + } + + /// + /// Original: https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L239 + /// Applyies IDCT transformation on "s" copying transformed values to "d", using temporal block "temp" + /// + /// + /// + /// + internal static void iDCT2D_llm(MutableSpan s, MutableSpan d, MutableSpan temp) + { + int j; + + for (j = 0; j < 8; j++) + { + iDCT1Dllm_32f(s.Slice(j * 8), temp.Slice(j * 8)); + } + + Transpose8x8(temp, d); + + for (j = 0; j < 8; j++) + { + iDCT1Dllm_32f(d.Slice(j * 8), temp.Slice(j * 8)); + } + + Transpose8x8(temp, d); + + for (j = 0; j < 64; j++) + { + d[j] *= 0.125f; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 _mm_load_ps(MutableSpan src, int offset) + { + src = src.Slice(offset); + return new Vector4(src[0], src[1], src[2], src[3]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void _mm_store_ps(MutableSpan dest, int offset, Vector4 src) + { + dest = dest.Slice(offset); + dest[0] = src.X; + dest[1] = src.Y; + dest[2] = src.Z; + dest[3] = src.W; + } + + private static readonly Vector4 _1_175876 = new Vector4(1.175876f); + + private static readonly Vector4 _1_961571 = new Vector4(-1.961571f); + + private static readonly Vector4 _0_390181 = new Vector4(-0.390181f); + + private static readonly Vector4 _0_899976 = new Vector4(-0.899976f); + + private static readonly Vector4 _2_562915 = new Vector4(-2.562915f); + + private static readonly Vector4 _0_298631 = new Vector4(0.298631f); + + private static readonly Vector4 _2_053120 = new Vector4(2.053120f); + + private static readonly Vector4 _3_072711 = new Vector4(3.072711f); + + private static readonly Vector4 _1_501321 = new Vector4(1.501321f); + + private static readonly Vector4 _0_541196 = new Vector4(0.541196f); + + private static readonly Vector4 _1_847759 = new Vector4(-1.847759f); + + private static readonly Vector4 _0_765367 = new Vector4(0.765367f); + + /// + /// Original: + /// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261 + /// Does a part of the IDCT job on the given parts of the blocks + /// + /// + /// + internal static void iDCT2D8x4_32f(MutableSpan y, MutableSpan x) + { + /* + float a0,a1,a2,a3,b0,b1,b2,b3; float z0,z1,z2,z3,z4; float r[8]; int i; + for(i = 0;i < 8;i++){ r[i] = (float)(cos((double)i / 16.0 * M_PI) * M_SQRT2); } + */ + /* + 0: 1.414214 + 1: 1.387040 + 2: 1.306563 + 3: + 4: 1.000000 + 5: 0.785695 + 6: + 7: 0.275899 + */ + + Vector4 my1 = _mm_load_ps(y, 8); + Vector4 my7 = _mm_load_ps(y, 56); + Vector4 mz0 = my1 + my7; + + Vector4 my3 = _mm_load_ps(y, 24); + Vector4 mz2 = my3 + my7; + Vector4 my5 = _mm_load_ps(y, 40); + Vector4 mz1 = my3 + my5; + Vector4 mz3 = my1 + my5; + + Vector4 mz4 = ((mz0 + mz1) * _1_175876); + //z0 = y[1] + y[7]; z1 = y[3] + y[5]; z2 = y[3] + y[7]; z3 = y[1] + y[5]; + //z4 = (z0 + z1) * r[3]; + + mz2 = mz2 * _1_961571 + mz4; + mz3 = mz3 * _0_390181 + mz4; + mz0 = mz0 * _0_899976; + mz1 = mz1 * _2_562915; + + /* + -0.899976 + -2.562915 + -1.961571 + -0.390181 + z0 = z0 * (-r[3] + r[7]); + z1 = z1 * (-r[3] - r[1]); + z2 = z2 * (-r[3] - r[5]) + z4; + z3 = z3 * (-r[3] + r[5]) + z4;*/ + + Vector4 mb3 = my7 * _0_298631 + mz0 + mz2; + Vector4 mb2 = my5 * _2_053120 + mz1 + mz3; + Vector4 mb1 = my3 * _3_072711 + mz1 + mz2; + Vector4 mb0 = my1 * _1_501321 + mz0 + mz3; + + /* + 0.298631 + 2.053120 + 3.072711 + 1.501321 + b3 = y[7] * (-r[1] + r[3] + r[5] - r[7]) + z0 + z2; + b2 = y[5] * ( r[1] + r[3] - r[5] + r[7]) + z1 + z3; + b1 = y[3] * ( r[1] + r[3] + r[5] - r[7]) + z1 + z2; + b0 = y[1] * ( r[1] + r[3] - r[5] - r[7]) + z0 + z3; + */ + + Vector4 my2 = _mm_load_ps(y, 16); + Vector4 my6 = _mm_load_ps(y, 48); + mz4 = (my2 + my6) * _0_541196; + Vector4 my0 = _mm_load_ps(y, 0); + Vector4 my4 = _mm_load_ps(y, 32); + mz0 = my0 + my4; + mz1 = my0 - my4; + + mz2 = mz4 + my6 * _1_847759; + mz3 = mz4 + my2 * _0_765367; + + my0 = mz0 + mz3; + my3 = mz0 - mz3; + my1 = mz1 + mz2; + my2 = mz1 - mz2; + /* + 1.847759 + 0.765367 + z4 = (y[2] + y[6]) * r[6]; + z0 = y[0] + y[4]; z1 = y[0] - y[4]; + z2 = z4 - y[6] * (r[2] + r[6]); + z3 = z4 + y[2] * (r[2] - r[6]); + a0 = z0 + z3; a3 = z0 - z3; + a1 = z1 + z2; a2 = z1 - z2; + */ + + _mm_store_ps(x, 0, my0 + mb0); + + _mm_store_ps(x, 56, my0 - mb0); + + _mm_store_ps(x, 8, my1 + mb1); + + _mm_store_ps(x, 48, my1 - mb1); + + _mm_store_ps(x, 16, my2 + mb2); + + _mm_store_ps(x, 40, my2 - mb2); + + _mm_store_ps(x, 24, my3 + mb3); + + _mm_store_ps(x, 32, my3 - mb3); + /* + x[0] = a0 + b0; x[7] = a0 - b0; + x[1] = a1 + b1; x[6] = a1 - b1; + x[2] = a2 + b2; x[5] = a2 - b2; + x[3] = a3 + b3; x[4] = a3 - b3; + for(i = 0;i < 8;i++){ x[i] *= 0.353554f; } + */ + } + + /// + /// Copies color values from block to the destination image buffer. + /// + /// + /// + /// + internal static unsafe void CopyColorsTo(ref Block8x8F block, MutableSpan buffer, int stride) + { + fixed (Block8x8F* p = &block) + { + float* b = (float*)p; + + for (int y = 0; y < 8; y++) + { + int y8 = y * 8; + int yStride = y * stride; + + for (int x = 0; x < 8; x++) + { + float c = b[y8 + x]; + + if (c < -128) + { + c = 0; + } + else if (c > 127) + { + c = 255; + } + else + { + c += 128; + } + + buffer[yStride + x] = (byte)c; + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs b/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs new file mode 100644 index 0000000000..55e609a524 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/UtilityTestClassBase.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using ImageSharp.Formats; +using Xunit.Abstractions; + +namespace ImageSharp.Tests.Formats.Jpg +{ + public class UtilityTestClassBase + { + public UtilityTestClassBase(ITestOutputHelper output) + { + Output = output; + } + + protected ITestOutputHelper Output { get; } + + // 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; + } + + internal void Print8x8Data(MutableSpan data) => Print8x8Data(data.Data); + + internal void Print8x8Data(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(); + } + + Output.WriteLine(bld.ToString()); + } + + internal void PrintLinearData(T[] data) => PrintLinearData(new MutableSpan(data), data.Length); + + internal void PrintLinearData(MutableSpan 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} "); + } + Output.WriteLine(bld.ToString()); + } + + protected void Measure(int times, Action action, [CallerMemberName] string operationName = null) + { + Output.WriteLine($"{operationName} X {times} ..."); + Stopwatch sw = Stopwatch.StartNew(); + + for (int i = 0; i < times; i++) + { + action(); + } + + sw.Stop(); + Output.WriteLine($"{operationName} finished in {sw.ElapsedMilliseconds} ms"); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTests.cs index 0df3e58184..a76cbc615f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTests.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // +using ImageSharp.Formats; + namespace ImageSharp.Tests { using System.IO; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fcd8d27cd8..05d18d80a5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -38,13 +38,18 @@ namespace ImageSharp.Tests { private static readonly string folder = "TestImages/Formats/Jpg/"; public static string Cmyk => folder + "cmyk.jpg"; - public static string Exif => folder + "exif.jpeg"; + public static string Exif => folder + "exif.jpg"; public static string Floorplan => folder + "Floorplan.jpeg"; public static string Calliphora => folder + "Calliphora.jpg"; public static string Turtle => folder + "turtle.jpg"; public static string Fb => folder + "fb.jpg"; public static string Progress => folder + "progress.jpg"; public static string GammaDalaiLamaGray => folder + "gamma_dalai_lama_gray.jpg"; + + + public static readonly string[] All = { + Cmyk, Exif, Floorplan, Calliphora, Turtle, Fb, Progress, GammaDalaiLamaGray + }; } public static class Bmp @@ -56,6 +61,10 @@ namespace ImageSharp.Tests public static string F => folder + "F.bmp"; public static string NegHeight => folder + "neg_height.bmp"; + + public static readonly string[] All = { + Car, F, NegHeight + }; } public static class Gif