diff --git a/ImageSharp.sln b/ImageSharp.sln index a584c5686..aedfcc345 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -49,9 +49,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{7CC6 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvatarWithRoundedCorner", "samples\AvatarWithRoundedCorner\AvatarWithRoundedCorner.csproj", "{844FC582-4E78-4371-847D-EFD4D1103578}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChangeDefaultEncoderOptions", "samples\ChangeDefaultEncoderOptions\ChangeDefaultEncoderOptions.csproj", "{07EE511D-4BAB-4323-BAFC-3AF2BF9366F0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeDefaultEncoderOptions", "samples\ChangeDefaultEncoderOptions\ChangeDefaultEncoderOptions.csproj", "{07EE511D-4BAB-4323-BAFC-3AF2BF9366F0}" EndProject Global + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 diff --git a/src/ImageSharp/Common/Extensions/ListExtensions.cs b/src/ImageSharp/Common/Extensions/ListExtensions.cs new file mode 100644 index 000000000..752f7ef21 --- /dev/null +++ b/src/ImageSharp/Common/Extensions/ListExtensions.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Common.Extensions +{ + using System.Collections.Generic; + + /// + /// Encapsulates a series of time saving extension methods to the class. + /// + internal static class ListExtensions + { + /// + /// Inserts an item at the given index automatically expanding the capacity if required. + /// + /// The type of object within the list + /// The list + /// The index + /// The item to insert + public static void SafeInsert(this List list, int index, T item) + { + if (index >= list.Count) + { + list.Add(item); + } + else + { + list[index] = item; + } + } + + /// + /// Removes the last element from a list and returns that element. This method changes the length of the list. + /// + /// The type of object within the list + /// The list + /// The last element in the specified sequence. + public static T Pop(this List list) + { + int last = list.Count - 1; + T item = list[last]; + list.RemoveAt(last); + return item; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 6de94dd22..6b474b92e 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -27,7 +27,7 @@ namespace ImageSharp if (stream.CanSeek) { - stream.Position += count; + stream.Seek(count, SeekOrigin.Current); // Position += count; } else { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index b3caddeca..38160c148 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -5,8 +5,6 @@ namespace ImageSharp.Formats { - using System; - using System.Collections.Generic; using System.IO; using ImageSharp.PixelFormats; @@ -27,10 +25,11 @@ namespace ImageSharp.Formats { Guard.NotNull(stream, "stream"); - using (JpegDecoderCore decoder = new JpegDecoderCore(configuration, this)) + // using (var decoder = new JpegDecoderCore(configuration, this)) + using (var decoder = new Jpeg.Port.JpegDecoderCore(configuration, this)) { return decoder.Decode(stream); } } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs new file mode 100644 index 000000000..6ef128ccb --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + /// + /// Provides information about the Adobe marker segment + /// + internal struct Adobe : IEquatable + { + /// + /// The DCT Encode Version + /// + public short DCTEncodeVersion; + + /// + /// 0x0 : (none) + /// Bit 15 : Encoded with Blend=1 downsampling + /// + public short APP14Flags0; + + /// + /// 0x0 : (none) + /// + public short APP14Flags1; + + /// + /// Determines the colorspace transform + /// 00 : Unknown (RGB or CMYK) + /// 01 : YCbCr + /// 02 : YCCK + /// + public byte ColorTransform; + + /// + public bool Equals(Adobe other) + { + return this.DCTEncodeVersion == other.DCTEncodeVersion + && this.APP14Flags0 == other.APP14Flags0 + && this.APP14Flags1 == other.APP14Flags1 + && this.ColorTransform == other.ColorTransform; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is Adobe && this.Equals((Adobe)obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + // TODO: Merge and use HashCodeHelpers + int hashCode = this.DCTEncodeVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ this.APP14Flags0.GetHashCode(); + hashCode = (hashCode * 397) ^ this.APP14Flags1.GetHashCode(); + hashCode = (hashCode * 397) ^ this.ColorTransform.GetHashCode(); + return hashCode; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs new file mode 100644 index 000000000..a21cb6620 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Numerics; + using ImageSharp.Memory; + + /// + /// Represents a component block + /// + internal struct Component : IDisposable + { + /// + /// Gets or sets the output + /// + public Buffer Output; + + /// + /// Gets or sets the scaling factors + /// + public Vector2 Scale; + + /// + /// Gets or sets the number of blocks per line + /// + public int BlocksPerLine; + + /// + /// Gets or sets the number of blocks per column + /// + public int BlocksPerColumn; + + /// + public void Dispose() + { + this.Output?.Dispose(); + this.Output = null; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs new file mode 100644 index 000000000..a72835e75 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + /// + /// Contains all the decoded component blocks + /// + internal sealed class ComponentBlocks : IDisposable + { + /// + /// Gets or sets the component blocks + /// + public Component[] Components { get; set; } + + /// + public void Dispose() + { + if (this.Components != null) + { + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i].Dispose(); + } + + this.Components = null; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs new file mode 100644 index 000000000..eaf3dafb9 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Represents a jpeg file marker + /// + internal struct FileMarker + { + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + public FileMarker(ushort marker, long position) + { + this.Marker = marker; + this.Position = position; + this.Invalid = false; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + /// Whether the current marker is invalid + public FileMarker(ushort marker, long position, bool invalid) + { + this.Marker = marker; + this.Position = position; + this.Invalid = invalid; + } + + /// + /// Gets or sets a value indicating whether the current marker is invalid + /// + public bool Invalid { get; set; } + + /// + /// Gets the position of the marker within a stream + /// + public ushort Marker { get; } + + /// + /// Gets the position of the marker within a stream + /// + public long Position { get; } + + /// + public override string ToString() + { + return this.Marker.ToString("X"); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs new file mode 100644 index 000000000..06b4bbc24 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + /// + /// Represent a single jpeg frame + /// + internal sealed class Frame : IDisposable + { + /// + /// Gets or sets a value indicating whether the frame uses the extended specification + /// + public bool Extended { get; set; } + + /// + /// Gets or sets a value indicating whether the frame uses the progressive specification + /// + public bool Progressive { get; set; } + + /// + /// Gets or sets the precision + /// + public byte Precision { get; set; } + + /// + /// Gets or sets the number of scanlines within the frame + /// + public short Scanlines { get; set; } + + /// + /// Gets or sets the number of samples per scanline + /// + public short SamplesPerLine { get; set; } + + /// + /// Gets or sets the number of components within a frame. In progressive frames this value can range from only 1 to 4 + /// + public byte ComponentCount { get; set; } + + /// + /// Gets or sets the component id collection + /// + public byte[] ComponentIds { get; set; } + + /// + /// Gets or sets the frame component collection + /// + public FrameComponent[] Components { get; set; } + + /// + /// Gets or sets the maximum horizontal sampling factor + /// + public int MaxHorizontalFactor { get; set; } + + /// + /// Gets or sets the maximum vertical sampling factor + /// + public int MaxVerticalFactor { get; set; } + + /// + /// Gets or sets the number of MCU's per line + /// + public int McusPerLine { get; set; } + + /// + /// Gets or sets the number of MCU's per column + /// + public int McusPerColumn { get; set; } + + /// + public void Dispose() + { + if (this.Components != null) + { + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i].Dispose(); + } + + this.Components = null; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs new file mode 100644 index 000000000..b386a86f3 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + using ImageSharp.Memory; + + /// + /// Represents a single frame component + /// + internal struct FrameComponent : IDisposable + { + /// + /// Gets or sets the component Id + /// + public byte Id; + + /// + /// TODO: What does pred stand for? + /// + public int Pred; + + /// + /// Gets or sets the horizontal sampling factor. + /// + public int HorizontalFactor; + + /// + /// Gets or sets the vertical sampling factor. + /// + public int VerticalFactor; + + /// + /// Gets or sets the identifier + /// + public byte QuantizationIdentifier; + + /// + /// Gets or sets the block data + /// + public Buffer BlockData; + + /// + /// Gets or sets the number of blocks per line + /// + public int BlocksPerLine; + + /// + /// Gets or sets the number of blocks per column + /// + public int BlocksPerColumn; + + /// + /// Gets the index for the DC Huffman table + /// + public int DCHuffmanTableId; + + /// + /// Gets the index for the AC Huffman table + /// + public int ACHuffmanTableId; + + /// + public void Dispose() + { + this.BlockData?.Dispose(); + this.BlockData = null; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs new file mode 100644 index 000000000..4c475450b --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs @@ -0,0 +1,212 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Runtime.CompilerServices; + + using ImageSharp.Memory; + + /// + /// Represents a Huffman Table + /// + internal struct HuffmanTable : IDisposable + { + private Buffer lookahead; + private Buffer valOffset; + private Buffer maxcode; + private Buffer huffval; + + /// + /// Initializes a new instance of the struct. + /// + /// The code lengths + /// The huffman values + public HuffmanTable(byte[] lengths, byte[] values) + { + this.lookahead = Buffer.CreateClean(256); + this.valOffset = Buffer.CreateClean(18); + this.maxcode = Buffer.CreateClean(18); + + using (var huffsize = Buffer.CreateClean(257)) + using (var huffcode = Buffer.CreateClean(257)) + { + GenerateSizeTable(lengths, huffsize); + GenerateCodeTable(huffsize, huffcode); + GenerateDecoderTables(lengths, huffcode, this.valOffset, this.maxcode); + GenerateLookaheadTables(lengths, values, this.lookahead); + } + + this.huffval = Buffer.CreateClean(values.Length); + Buffer.BlockCopy(values, 0, this.huffval.Array, 0, values.Length); + + this.MaxCode = this.maxcode.Array; + this.ValOffset = this.valOffset.Array; + this.HuffVal = this.huffval.Array; + this.Lookahead = this.lookahead.Array; + } + + /// + /// Gets the max code array + /// + public long[] MaxCode + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + /// Gets the value offset array + /// + public short[] ValOffset + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + /// Gets the huffman value array + /// + public byte[] HuffVal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + /// Gets the lookahead array + /// + public short[] Lookahead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + public void Dispose() + { + this.lookahead?.Dispose(); + this.valOffset?.Dispose(); + this.maxcode?.Dispose(); + this.huffval?.Dispose(); + + this.lookahead = null; + this.valOffset = null; + this.maxcode = null; + this.huffval = null; + } + + /// + /// Figure C.1: make table of Huffman code length for each symbol + /// + /// The code lengths + /// The huffman size span + private static void GenerateSizeTable(byte[] lengths, Span huffsize) + { + short index = 0; + for (short l = 1; l <= 16; l++) + { + byte i = lengths[l]; + for (short j = 0; j < i; j++) + { + huffsize[index] = l; + index++; + } + } + + huffsize[index] = 0; + } + + /// + /// Figure C.2: generate the codes themselves + /// + /// The huffman size span + /// The huffman code span + private static void GenerateCodeTable(Span huffsize, Span huffcode) + { + short k = 0; + short si = huffsize[0]; + short code = 0; + for (short i = 0; i < huffsize.Length; i++) + { + while (huffsize[k] == si) + { + huffcode[k] = code; + code++; + k++; + } + + code <<= 1; + si++; + } + } + + /// + /// Figure F.15: generate decoding tables for bit-sequential decoding + /// + /// The code lengths + /// The huffman code span + /// The value offset span + /// The max code span + private static void GenerateDecoderTables(byte[] lengths, Span huffcode, Span valOffset, Span maxcode) + { + short bitcount = 0; + for (int i = 1; i <= 16; i++) + { + if (lengths[i] != 0) + { + // valoffset[l] = huffval[] index of 1st symbol of code length i, + // minus the minimum code of length i + valOffset[i] = (short)(bitcount - huffcode[bitcount]); + bitcount += lengths[i]; + maxcode[i] = huffcode[bitcount - 1]; // maximum code of length i + } + else + { + maxcode[i] = -1; // -1 if no codes of this length + } + } + + valOffset[17] = 0; + maxcode[17] = 0xFFFFFL; + } + + /// + /// Generates lookup tables to speed up decoding + /// + /// The code lengths + /// The huffman value array + /// The lookahead span + private static void GenerateLookaheadTables(byte[] lengths, byte[] huffval, Span lookahead) + { + int x = 0, code = 0; + + for (int i = 0; i < 8; i++) + { + code <<= 1; + + for (int j = 0; j < lengths[i + 1]; j++) + { + // The codeLength is 1+i, so shift code by 8-(1+i) to + // calculate the high bits for every 8-bit sequence + // whose codeLength's high bits matches code. + // The high 8 bits of lutValue are the encoded value. + // The low 8 bits are 1 plus the codeLength. + byte base2 = (byte)(code << (7 - i)); + short lutValue = (short)((short)(huffval[x] << 8) | (short)(2 + i)); + + for (int k = 0; k < 1 << (7 - i); k++) + { + lookahead[base2 | k] = lutValue; + } + + code++; + x++; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs new file mode 100644 index 000000000..d076c0b03 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Collections.Generic; + using System.Runtime.CompilerServices; + + /// + /// Defines a pair of huffman tables + /// + internal sealed class HuffmanTables : IDisposable + { + private readonly HuffmanTable[] tables = new HuffmanTable[4]; + + /// + /// Gets or sets the table at the given index. + /// + /// The index + /// The + public ref HuffmanTable this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return ref this.tables[index]; + } + } + + /// + public void Dispose() + { + for (int i = 0; i < this.tables.Length; i++) + { + this.tables[i].Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs new file mode 100644 index 000000000..064b3bea3 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -0,0 +1,511 @@ +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Runtime.CompilerServices; + + using ImageSharp.Memory; + + /// + /// Performs the inverse Descrete Cosine Transform on each frame component. + /// + internal static class IDCT + { + /// + /// Precomputed values scaled up by 14 bits + /// + public static readonly short[] Aanscales = + { + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 22725, 31521, 29692, 26722, 22725, 17855, + 12299, 6270, 21407, 29692, 27969, 25172, 21407, 16819, 11585, + 5906, 19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315, + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 12873, + 17855, 16819, 15137, 12873, 10114, 6967, 3552, 8867, 12299, + 11585, 10426, 8867, 6967, 4799, 2446, 4520, 6270, 5906, 5315, + 4520, 3552, 2446, 1247 + }; + + private const int DctCos1 = 4017; // cos(pi/16) + private const int DctSin1 = 799; // sin(pi/16) + private const int DctCos3 = 3406; // cos(3*pi/16) + private const int DctSin3 = 2276; // sin(3*pi/16) + private const int DctCos6 = 1567; // cos(6*pi/16) + private const int DctSin6 = 3784; // sin(6*pi/16) + private const int DctSqrt2 = 5793; // sqrt(2) + private const int DctSqrt1D2 = 2896; // sqrt(2) / 2 + +#pragma warning disable SA1310 // Field names must not contain underscore + private const int FIX_1_082392200 = 277; // FIX(1.082392200) + private const int FIX_1_414213562 = 362; // FIX(1.414213562) + private const int FIX_1_847759065 = 473; // FIX(1.847759065) + private const int FIX_2_613125930 = 669; // FIX(2.613125930) +#pragma warning restore SA1310 // Field names must not contain underscore + + private const int ConstBits = 8; + private const int Pass1Bits = 2; // Factional bits in scale factors + private const int MaxJSample = 255; + private const int CenterJSample = 128; + private const int RangeCenter = (MaxJSample * 2) + 2; + + // First segment of range limit table: limit[x] = 0 for x < 0 + // allow negative subscripts of simple table + private const int TableOffset = 2 * (MaxJSample + 1); + private const int LimitOffset = TableOffset - (RangeCenter - CenterJSample); + + // Each IDCT routine is responsible for range-limiting its results and + // converting them to unsigned form (0..MaxJSample). The raw outputs could + // be quite far out of range if the input data is corrupt, so a bulletproof + // range-limiting step is required. We use a mask-and-table-lookup method + // to do the combined operations quickly, assuming that MaxJSample+1 + // is a power of 2. + private const int RangeMask = (MaxJSample * 4) + 3; // 2 bits wider than legal samples + + private static readonly byte[] Limit = new byte[5 * (MaxJSample + 1)]; + + static IDCT() + { + // Main part of range limit table: limit[x] = x + int i; + for (i = 0; i <= MaxJSample; i++) + { + Limit[TableOffset + i] = (byte)i; + } + + // End of range limit table: Limit[x] = MaxJSample for x > MaxJSample + for (; i < 3 * (MaxJSample + 1); i++) + { + Limit[TableOffset + i] = MaxJSample; + } + } + + /// + /// A port of Poppler's IDCT method which in turn is taken from: + /// Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + /// 'Practical Fast 1-D DCT Algorithms with 11 Multiplications', + /// IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, 988-991. + /// + /// The fram component + /// The block buffer offset + /// The computational buffer for holding temp values + /// The quantization table + public static void QuantizeAndInverse(ref FrameComponent component, int blockBufferOffset, ref Span computationBuffer, ref Span quantizationTable) + { + Span blockData = component.BlockData.Slice(blockBufferOffset); + int v0, v1, v2, v3, v4, v5, v6, v7; + int p0, p1, p2, p3, p4, p5, p6, p7; + int t; + + // inverse DCT on rows + for (int row = 0; row < 64; row += 8) + { + // gather block data + p0 = blockData[row]; + p1 = blockData[row + 1]; + p2 = blockData[row + 2]; + p3 = blockData[row + 3]; + p4 = blockData[row + 4]; + p5 = blockData[row + 5]; + p6 = blockData[row + 6]; + p7 = blockData[row + 7]; + + // dequant p0 + p0 *= quantizationTable[row]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + t = ((DctSqrt2 * p0) + 512) >> 10; + short st = (short)t; + computationBuffer[row] = st; + computationBuffer[row + 1] = st; + computationBuffer[row + 2] = st; + computationBuffer[row + 3] = st; + computationBuffer[row + 4] = st; + computationBuffer[row + 5] = st; + computationBuffer[row + 6] = st; + computationBuffer[row + 7] = st; + continue; + } + + // dequant p1 ... p7 + p1 *= quantizationTable[row + 1]; + p2 *= quantizationTable[row + 2]; + p3 *= quantizationTable[row + 3]; + p4 *= quantizationTable[row + 4]; + p5 *= quantizationTable[row + 5]; + p6 *= quantizationTable[row + 6]; + p7 *= quantizationTable[row + 7]; + + // stage 4 + v0 = ((DctSqrt2 * p0) + 128) >> 8; + v1 = ((DctSqrt2 * p4) + 128) >> 8; + v2 = p2; + v3 = p6; + v4 = ((DctSqrt1D2 * (p1 - p7)) + 128) >> 8; + v7 = ((DctSqrt1D2 * (p1 + p7)) + 128) >> 8; + v5 = p3 << 4; + v6 = p5 << 4; + + // stage 3 + v0 = (v0 + v1 + 1) >> 1; + v1 = v0 - v1; + t = ((v2 * DctSin6) + (v3 * DctCos6) + 128) >> 8; + v2 = ((v2 * DctCos6) - (v3 * DctSin6) + 128) >> 8; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = ((v4 * DctSin3) + (v7 * DctCos3) + 2048) >> 12; + v4 = ((v4 * DctCos3) - (v7 * DctSin3) + 2048) >> 12; + v7 = t; + t = ((v5 * DctSin1) + (v6 * DctCos1) + 2048) >> 12; + v5 = ((v5 * DctCos1) - (v6 * DctSin1) + 2048) >> 12; + v6 = t; + + // stage 1 + computationBuffer[row] = (short)(v0 + v7); + computationBuffer[row + 7] = (short)(v0 - v7); + computationBuffer[row + 1] = (short)(v1 + v6); + computationBuffer[row + 6] = (short)(v1 - v6); + computationBuffer[row + 2] = (short)(v2 + v5); + computationBuffer[row + 5] = (short)(v2 - v5); + computationBuffer[row + 3] = (short)(v3 + v4); + computationBuffer[row + 4] = (short)(v3 - v4); + } + + // inverse DCT on columns + for (int col = 0; col < 8; ++col) + { + p0 = computationBuffer[col]; + p1 = computationBuffer[col + 8]; + p2 = computationBuffer[col + 16]; + p3 = computationBuffer[col + 24]; + p4 = computationBuffer[col + 32]; + p5 = computationBuffer[col + 40]; + p6 = computationBuffer[col + 48]; + p7 = computationBuffer[col + 56]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + t = ((DctSqrt2 * p0) + 8192) >> 14; + + // convert to 8 bit + t = (t < -2040) ? 0 : (t >= 2024) ? MaxJSample : (t + 2056) >> 4; + short st = (short)t; + + blockData[col] = st; + blockData[col + 8] = st; + blockData[col + 16] = st; + blockData[col + 24] = st; + blockData[col + 32] = st; + blockData[col + 40] = st; + blockData[col + 48] = st; + blockData[col + 56] = st; + continue; + } + + // stage 4 + v0 = ((DctSqrt2 * p0) + 2048) >> 12; + v1 = ((DctSqrt2 * p4) + 2048) >> 12; + v2 = p2; + v3 = p6; + v4 = ((DctSqrt1D2 * (p1 - p7)) + 2048) >> 12; + v7 = ((DctSqrt1D2 * (p1 + p7)) + 2048) >> 12; + v5 = p3; + v6 = p5; + + // stage 3 + // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when + // converting to UInt8 range later. + v0 = ((v0 + v1 + 1) >> 1) + 4112; + v1 = v0 - v1; + t = ((v2 * DctSin6) + (v3 * DctCos6) + 2048) >> 12; + v2 = ((v2 * DctCos6) - (v3 * DctSin6) + 2048) >> 12; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = ((v4 * DctSin3) + (v7 * DctCos3) + 2048) >> 12; + v4 = ((v4 * DctCos3) - (v7 * DctSin3) + 2048) >> 12; + v7 = t; + t = ((v5 * DctSin1) + (v6 * DctCos1) + 2048) >> 12; + v5 = ((v5 * DctCos1) - (v6 * DctSin1) + 2048) >> 12; + v6 = t; + + // stage 1 + p0 = v0 + v7; + p7 = v0 - v7; + p1 = v1 + v6; + p6 = v1 - v6; + p2 = v2 + v5; + p5 = v2 - v5; + p3 = v3 + v4; + p4 = v3 - v4; + + // convert to 8-bit integers + p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? MaxJSample : p0 >> 4; + p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? MaxJSample : p1 >> 4; + p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? MaxJSample : p2 >> 4; + p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? MaxJSample : p3 >> 4; + p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? MaxJSample : p4 >> 4; + p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? MaxJSample : p5 >> 4; + p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? MaxJSample : p6 >> 4; + p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? MaxJSample : p7 >> 4; + + // store block data + blockData[col] = (short)p0; + blockData[col + 8] = (short)p1; + blockData[col + 16] = (short)p2; + blockData[col + 24] = (short)p3; + blockData[col + 32] = (short)p4; + blockData[col + 40] = (short)p5; + blockData[col + 48] = (short)p6; + blockData[col + 56] = (short)p7; + } + } + + /// + /// A port of + /// A 2-D IDCT can be done by 1-D IDCT on each column followed by 1-D IDCT + /// on each row(or vice versa, but it's more convenient to emit a row at + /// a time). Direct algorithms are also available, but they are much more + /// complex and seem not to be any faster when reduced to code. + /// + /// This implementation is based on Arai, Agui, and Nakajima's algorithm for + /// scaled DCT.Their original paper (Trans.IEICE E-71(11):1095) is in + /// Japanese, but the algorithm is described in the Pennebaker & Mitchell + /// JPEG textbook(see REFERENCES section in file README.ijg). The following + /// code is based directly on figure 4-8 in P&M. + /// While an 8-point DCT cannot be done in less than 11 multiplies, it is + /// possible to arrange the computation so that many of the multiplies are + /// simple scalings of the final outputs.These multiplies can then be + /// folded into the multiplications or divisions by the JPEG quantization + /// table entries. The AA&N method leaves only 5 multiplies and 29 adds + /// to be done in the DCT itself. + /// The primary disadvantage of this method is that with fixed-point math, + /// accuracy is lost due to imprecise representation of the scaled + /// quantization values.The smaller the quantization table entry, the less + /// precise the scaled value, so this implementation does worse with high - + /// quality - setting files than with low - quality ones. + /// + /// The frame component + /// The block buffer offset + /// The computational buffer for holding temp values + /// The multiplier table + public static void QuantizeAndInverseFast(ref FrameComponent component, int blockBufferOffset, ref Span computationBuffer, ref Span multiplierTable) + { + Span blockData = component.BlockData.Slice(blockBufferOffset); + int p0, p1, p2, p3, p4, p5, p6, p7; + + for (int col = 0; col < 8; col++) + { + // Gather block data + p0 = blockData[col]; + p1 = blockData[col + 8]; + p2 = blockData[col + 16]; + p3 = blockData[col + 24]; + p4 = blockData[col + 32]; + p5 = blockData[col + 40]; + p6 = blockData[col + 48]; + p7 = blockData[col + 56]; + + int tmp0 = p0 * multiplierTable[col]; + + // Due to quantization, we will usually find that many of the input + // coefficients are zero, especially the AC terms. We can exploit this + // by short-circuiting the IDCT calculation for any column in which all + // the AC terms are zero. In that case each output is equal to the + // DC coefficient (with scale factor as needed). + // With typical images and quantization tables, half or more of the + // column DCT calculations can be simplified this way. + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + short dcval = (short)tmp0; + + computationBuffer[col] = dcval; + computationBuffer[col + 8] = dcval; + computationBuffer[col + 16] = dcval; + computationBuffer[col + 24] = dcval; + computationBuffer[col + 32] = dcval; + computationBuffer[col + 40] = dcval; + computationBuffer[col + 48] = dcval; + computationBuffer[col + 56] = dcval; + + continue; + } + + // Even part + int tmp1 = p2 * multiplierTable[col + 16]; + int tmp2 = p4 * multiplierTable[col + 32]; + int tmp3 = p6 * multiplierTable[col + 48]; + + int tmp10 = tmp0 + tmp2; // Phase 3 + int tmp11 = tmp0 - tmp2; + + int tmp13 = tmp1 + tmp3; // Phases 5-3 + int tmp12 = Multiply(tmp1 - tmp3, FIX_1_414213562) - tmp13; // 2*c4 + + tmp0 = tmp10 + tmp13; // Phase 2 + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd Part + int tmp4 = p1 * multiplierTable[col + 8]; + int tmp5 = p3 * multiplierTable[col + 24]; + int tmp6 = p5 * multiplierTable[col + 40]; + int tmp7 = p7 * multiplierTable[col + 56]; + + int z13 = tmp6 + tmp5; // Phase 6 + int z10 = tmp6 - tmp5; + int z11 = tmp4 + tmp7; + int z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) + tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) + + tmp6 = tmp12 - tmp7; // Phase 2 + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 - tmp5; + + computationBuffer[col] = (short)(tmp0 + tmp7); + computationBuffer[col + 56] = (short)(tmp0 - tmp7); + computationBuffer[col + 8] = (short)(tmp1 + tmp6); + computationBuffer[col + 48] = (short)(tmp1 - tmp6); + computationBuffer[col + 16] = (short)(tmp2 + tmp5); + computationBuffer[col + 40] = (short)(tmp2 - tmp5); + computationBuffer[col + 24] = (short)(tmp3 + tmp4); + computationBuffer[col + 32] = (short)(tmp3 - tmp4); + } + + // Pass 2: process rows from work array, store into output array. + // Note that we must descale the results by a factor of 8 == 2**3, + // and also undo the pass 1 bits scaling. + for (int row = 0; row < 64; row += 8) + { + p1 = computationBuffer[row + 1]; + p2 = computationBuffer[row + 2]; + p3 = computationBuffer[row + 3]; + p4 = computationBuffer[row + 4]; + p5 = computationBuffer[row + 5]; + p6 = computationBuffer[row + 6]; + p7 = computationBuffer[row + 7]; + + // Add range center and fudge factor for final descale and range-limit. + int z5 = computationBuffer[row] + (RangeCenter << (Pass1Bits + 3)) + (1 << (Pass1Bits + 2)); + + // Check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + byte dcval = Limit[LimitOffset + (RightShift(z5, Pass1Bits + 3) & RangeMask)]; + + blockData[row] = dcval; + blockData[row + 1] = dcval; + blockData[row + 2] = dcval; + blockData[row + 3] = dcval; + blockData[row + 4] = dcval; + blockData[row + 5] = dcval; + blockData[row + 6] = dcval; + blockData[row + 7] = dcval; + + continue; + } + + // Even part + int tmp10 = z5 + p4; + int tmp11 = z5 - p4; + + int tmp13 = p2 + p6; + int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; // 2*c4 + + int tmp0 = tmp10 + tmp13; + int tmp3 = tmp10 - tmp13; + int tmp1 = tmp11 + tmp12; + int tmp2 = tmp11 - tmp12; + + // Odd part + int z13 = p5 + p3; + int z10 = p5 - p3; + int z11 = p1 + p7; + int z12 = p1 - p7; + + int tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) + tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) + + int tmp6 = tmp12 - tmp7; // Phase 2 + int tmp5 = tmp11 - tmp6; + int tmp4 = tmp10 - tmp5; + + // Final output stage: scale down by a factor of 8, offset, and range-limit + blockData[row] = Limit[LimitOffset + (RightShift(tmp0 + tmp7, Pass1Bits + 3) & RangeMask)]; + blockData[row + 7] = Limit[LimitOffset + (RightShift(tmp0 - tmp7, Pass1Bits + 3) & RangeMask)]; + blockData[row + 1] = Limit[LimitOffset + (RightShift(tmp1 + tmp6, Pass1Bits + 3) & RangeMask)]; + blockData[row + 6] = Limit[LimitOffset + (RightShift(tmp1 - tmp6, Pass1Bits + 3) & RangeMask)]; + blockData[row + 2] = Limit[LimitOffset + (RightShift(tmp2 + tmp5, Pass1Bits + 3) & RangeMask)]; + blockData[row + 5] = Limit[LimitOffset + (RightShift(tmp2 - tmp5, Pass1Bits + 3) & RangeMask)]; + blockData[row + 3] = Limit[LimitOffset + (RightShift(tmp3 + tmp4, Pass1Bits + 3) & RangeMask)]; + blockData[row + 4] = Limit[LimitOffset + (RightShift(tmp3 - tmp4, Pass1Bits + 3) & RangeMask)]; + } + } + + /// + /// Descale and correctly round an int value that's scaled by bits. + /// We assume rounds towards minus infinity, so adding + /// the fudge factor is correct for either sign of . + /// + /// The value + /// The number of bits + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Descale(int value, int n) + { + return RightShift(value + (1 << (n - 1)), n); + } + + /// + /// Multiply a variable by an int constant, and immediately descale. + /// + /// The value + /// The multiplier + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Multiply(int val, int c) + { + return Descale(val * c, ConstBits); + } + + /// + /// Right-shifts the value by the given amount + /// + /// The value + /// The amount to shift by + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RightShift(int value, int shift) + { + return value >> shift; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs new file mode 100644 index 000000000..6baecdf68 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + /// + /// Provides information about the JFIF marker segment + /// TODO: Thumbnail? + /// + internal struct JFif : IEquatable + { + /// + /// The major version + /// + public byte MajorVersion; + + /// + /// The minor version + /// + public byte MinorVersion; + + /// + /// Units for the following pixel density fields + /// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity + /// 01 : Pixels per inch (2.54 cm) + /// 02 : Pixels per centimeter + /// + public byte DensityUnits; + + /// + /// Horizontal pixel density. Must not be zero. + /// + public short XDensity; + + /// + /// Vertical pixel density. Must not be zero. + /// + public short YDensity; + + /// + public bool Equals(JFif other) + { + return this.MajorVersion == other.MajorVersion + && this.MinorVersion == other.MinorVersion + && this.DensityUnits == other.DensityUnits + && this.XDensity == other.XDensity + && this.YDensity == other.YDensity; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is JFif && this.Equals((JFif)obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = this.MajorVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ this.MinorVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ this.DensityUnits.GetHashCode(); + hashCode = (hashCode * 397) ^ this.XDensity.GetHashCode(); + hashCode = (hashCode * 397) ^ this.YDensity.GetHashCode(); + return hashCode; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs new file mode 100644 index 000000000..e88e39617 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs @@ -0,0 +1,147 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Diagnostics; + using System.Numerics; + using System.Runtime.CompilerServices; + using ImageSharp.Memory; + + /// + /// Represents a section of the jpeg component data laid out in pixel order. + /// + internal struct JpegPixelArea : IDisposable + { + private readonly int imageWidth; + + private readonly int imageHeight; + + private Buffer componentData; + + private int rowStride; + + /// + /// Initializes a new instance of the struct. + /// + /// The image width + /// The image height + /// The number of components + public JpegPixelArea(int imageWidth, int imageHeight, int numberOfComponents) + { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.Width = 0; + this.Height = 0; + this.NumberOfComponents = numberOfComponents; + this.componentData = null; + this.rowStride = 0; + } + + /// + /// Gets the number of components + /// + public int NumberOfComponents { get; } + + /// + /// Gets the width + /// + public int Width { get; private set; } + + /// + /// Gets the height + /// + public int Height { get; private set; } + + /// + /// Organsizes the decoded jpeg components into a linear array ordered by component. + /// This must be called before attempting to retrieve the data. + /// + /// The jpeg component blocks + /// The pixel area width + /// The pixel area height + public void LinearizeBlockData(ComponentBlocks components, int width, int height) + { + this.Width = width; + this.Height = height; + int numberOfComponents = this.NumberOfComponents; + this.rowStride = width * numberOfComponents; + var scale = new Vector2(this.imageWidth / (float)width, this.imageHeight / (float)height); + + this.componentData = new Buffer(width * height * numberOfComponents); + Span componentDataSpan = this.componentData; + const uint Mask3Lsb = 0xFFFFFFF8; // Used to clear the 3 LSBs + + using (var xScaleBlockOffset = new Buffer(width)) + { + Span xScaleBlockOffsetSpan = xScaleBlockOffset; + for (int i = 0; i < numberOfComponents; i++) + { + ref Component component = ref components.Components[i]; + Vector2 componentScale = component.Scale * scale; + int offset = i; + Span output = component.Output; + int blocksPerScanline = (component.BlocksPerLine + 1) << 3; + + // Precalculate the xScaleBlockOffset + int j; + for (int x = 0; x < width; x++) + { + j = (int)(x * componentScale.X); + xScaleBlockOffsetSpan[x] = (int)((j & Mask3Lsb) << 3) | (j & 7); + } + + // Linearize the blocks of the component + for (int y = 0; y < height; y++) + { + j = (int)(y * componentScale.Y); + int index = blocksPerScanline * (int)(j & Mask3Lsb) | ((j & 7) << 3); + for (int x = 0; x < width; x++) + { + componentDataSpan[offset] = (byte)output[index + xScaleBlockOffsetSpan[x]]; + offset += numberOfComponents; + } + } + } + } + } + + /// + /// Gets a representing the row 'y' beginning from the the first byte on that row. + /// + /// The y-coordinate of the pixel row. Must be greater than or equal to zero and less than the height of the pixel area. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetRowSpan(int y) + { + this.CheckCoordinates(y); + return this.componentData.Slice(y * this.rowStride, this.rowStride); + } + + /// + public void Dispose() + { + this.componentData?.Dispose(); + this.componentData = null; + } + + /// + /// Checks the coordinates to ensure they are within bounds. + /// + /// The y-coordinate of the row. Must be greater than zero and less than the height of the area. + /// + /// Thrown if the coordinates are not within the bounds of the image. + /// + [Conditional("DEBUG")] + private void CheckCoordinates(int y) + { + if (y < 0 || y >= this.Height) + { + throw new ArgumentOutOfRangeException(nameof(y), y, $"{y} is outwith the area bounds."); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs new file mode 100644 index 000000000..352dc43f2 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Runtime.CompilerServices; + + using ImageSharp.Memory; + + /// + /// Contains the quantization tables. + /// + internal sealed class QuantizationTables : IDisposable + { + /// + /// Gets the ZigZag scan table + /// + public static byte[] DctZigZag + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + = + { + 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 + }; + + /// + /// Gets or sets the quantization tables. + /// + public Buffer2D Tables + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; set; + } + + = new Buffer2D(64, 4); + + /// + public void Dispose() + { + if (this.Tables != null) + { + this.Tables.Dispose(); + this.Tables = null; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs new file mode 100644 index 000000000..8b711eaa8 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -0,0 +1,947 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; +#if DEBUG + using System.Diagnostics; +#endif + using System.IO; + using System.Runtime.CompilerServices; + + /// + /// Provides the means to decode a spectral scan + /// + internal struct ScanDecoder + { + private byte[] markerBuffer; + + private int bitsData; + + private int bitsCount; + +#pragma warning disable 414 + private int bitsUnRead; + + private int accumulator; +#pragma warning restore 414 + + private int specStart; + + private int specEnd; + + private int eobrun; + + private int compIndex; + + private int successiveState; + + private int successiveACState; + + private int successiveACNextValue; + + private bool endOfStreamReached; + + private bool unexpectedMarkerReached; + + /// + /// Decodes the spectral scan + /// + /// The image frame + /// The input stream + /// The DC Huffman tables + /// The AC Huffman tables + /// The scan components + /// The component index within the array + /// The length of the components. Different to the array length + /// The reset interval + /// The spectral selection start + /// The spectral selection end + /// The successive approximation bit high end + /// The successive approximation bit low end + public void DecodeScan( + Frame frame, + Stream stream, + HuffmanTables dcHuffmanTables, + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentIndex, + int componentsLength, + ushort resetInterval, + int spectralStart, + int spectralEnd, + int successivePrev, + int successive) + { + this.markerBuffer = new byte[2]; + this.compIndex = componentIndex; + this.specStart = spectralStart; + this.specEnd = spectralEnd; + this.successiveState = successive; + this.endOfStreamReached = false; + this.unexpectedMarkerReached = false; + + bool progressive = frame.Progressive; + int mcusPerLine = frame.McusPerLine; + + int mcu = 0; + int mcuExpected; + if (componentsLength == 1) + { + mcuExpected = components[this.compIndex].BlocksPerLine * components[this.compIndex].BlocksPerColumn; + } + else + { + mcuExpected = mcusPerLine * frame.McusPerColumn; + } + + FileMarker fileMarker; + while (mcu < mcuExpected) + { + // Reset interval stuff + int mcuToRead = resetInterval != 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; + for (int i = 0; i < components.Length; i++) + { + ref FrameComponent c = ref components[i]; + c.Pred = 0; + } + + this.eobrun = 0; + + if (!progressive) + { + this.DecodeScanBaseline(dcHuffmanTables, acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + else + { + if (this.specStart == 0) + { + if (successivePrev == 0) + { + this.DecodeScanDCFirst(dcHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + else + { + this.DecodeScanDCSuccessive(components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + } + else + { + if (successivePrev == 0) + { + this.DecodeScanACFirst(acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + else + { + this.DecodeScanACSuccessive(acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + } + } + + // Find marker + this.bitsCount = 0; + this.accumulator = 0; + this.bitsUnRead = 0; + fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); + + // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker:X}"); +#endif + } + + ushort marker = fileMarker.Marker; + + // RSTn - We've alread read the bytes and altered the position so no need to skip + if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) + { + continue; + } + + if (!fileMarker.Invalid) + { + // We've found a valid marker. + // Rewind the stream to the position of the marker and break + stream.Position = fileMarker.Position; + break; + } + } + + fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); + + // Some images include more Scan blocks than expected, skip past those and + // attempt to find the next valid marker (fixes issue8182.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker:X}"); +#endif + } + else + { + // We've found a valid marker. + // Rewind the stream to the position of the marker + stream.Position = fileMarker.Position; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockBufferOffset(FrameComponent component, int row, int col) + { + return 64 * (((component.BlocksPerLine + 1) * row) + col); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanBaseline( + HuffmanTables dcHuffmanTables, + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + + for (int n = 0; n < mcuToRead; n++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeBlockBaseline(ref dcHuffmanTable, ref acHuffmanTable, ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeMcuBaseline(ref dcHuffmanTable, ref acHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanDCFirst( + HuffmanTables dcHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + + for (int n = 0; n < mcuToRead; n++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeBlockDCFirst(ref dcHuffmanTable, ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeMcuDCFirst(ref dcHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanDCSuccessive( + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + for (int n = 0; n < mcuToRead; n++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeBlockDCSuccessive(ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeMcuDCSuccessive(ref component, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanACFirst( + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + + for (int n = 0; n < mcuToRead; n++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeBlockACFirst(ref acHuffmanTable, ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeMcuACFirst(ref acHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanACSuccessive( + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + + for (int n = 0; n < mcuToRead; n++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeBlockACSuccessive(ref acHuffmanTable, ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + continue; + } + + this.DecodeMcuACSuccessive(ref acHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockBaseline(ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = mcu / component.BlocksPerLine; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeBaseline(ref component, offset, ref dcHuffmanTable, ref acHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuBaseline(ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = mcu / mcusPerLine; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeBaseline(ref component, offset, ref dcHuffmanTable, ref acHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockDCFirst(ref HuffmanTable dcHuffmanTable, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = mcu / component.BlocksPerLine; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCFirst(ref component, offset, ref dcHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuDCFirst(ref HuffmanTable dcHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = mcu / mcusPerLine; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCFirst(ref component, offset, ref dcHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockDCSuccessive(ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = mcu / component.BlocksPerLine; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCSuccessive(ref component, offset, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuDCSuccessive(ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = mcu / mcusPerLine; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCSuccessive(ref component, offset, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockACFirst(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = mcu / component.BlocksPerLine; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACFirst(ref component, offset, ref acHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuACFirst(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = mcu / mcusPerLine; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACFirst(ref component, offset, ref acHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockACSuccessive(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = mcu / component.BlocksPerLine; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACSuccessive(ref component, offset, ref acHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuACSuccessive(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = mcu / mcusPerLine; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACSuccessive(ref component, offset, ref acHuffmanTable, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadBit(Stream stream) + { + // TODO: I wonder if we can do this two bytes at a time; libjpeg turbo seems to do that? + if (this.bitsCount > 0) + { + this.bitsCount--; + return (this.bitsData >> this.bitsCount) & 1; + } + + this.bitsData = stream.ReadByte(); + + if (this.bitsData == -0x1) + { + // We've encountered the end of the file stream which means there's no EOI marker in the image + this.endOfStreamReached = true; + } + + if (this.bitsData == JpegConstants.Markers.Prefix) + { + int nextByte = stream.ReadByte(); + if (nextByte != 0) + { +#if DEBUG + Debug.WriteLine($"DecodeScan - Unexpected marker {(this.bitsData << 8) | nextByte:X} at {stream.Position}"); +#endif + + // We've encountered an unexpected marker. Reverse the stream and exit. + this.unexpectedMarkerReached = true; + stream.Position -= 2; + } + + // Unstuff 0 + } + + this.bitsCount = 7; + + return this.bitsData >> 7; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private short DecodeHuffman(ref HuffmanTable tree, Stream stream) + { + short code = -1; + + // TODO: Adding this code introduces error into the decoder. + // NOTES # During investigation of the libjpeg implementation it appears that they pull 32bits at a time and operate on those bits + // using 3 methods: FillBits, PeekBits, and ReadBits. We should attempt to do the same. + // It doesn't appear to speed anything up either. + // if (this.bitsUnRead < 8) + // { + // if (this.bitsCount <= 0) + // { + // code = (short)this.ReadBit(stream); + // if (this.endOfStreamReached || this.unexpectedMarkerReached) + // { + // return -1; + // } + // + // this.bitsUnRead += 8; + // } + // + // this.accumulator = (this.accumulator << 8) | this.bitsData; + // int lutIndex = (this.accumulator >> (8 - this.bitsUnRead)) & 0xFF; + // int v = tree.Lookahead[lutIndex]; + // if (v != 0) + // { + // int nb = (v & 0xFF) - 1; + // this.bitsCount -= nb - 1; + // this.bitsUnRead -= nb; + // v = v >> 8; + // return (short)v; + // } + // } + if (code == -1) + { + code = (short)this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return -1; + } + } + + // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 + int i = 1; + + while (code > tree.MaxCode[i]) + { + code <<= 1; + code |= (short)this.ReadBit(stream); + + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return -1; + } + + i++; + } + + int j = tree.ValOffset[i]; + return tree.HuffVal[(j + code) & 0xFF]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int Receive(int length, Stream stream) + { + int n = 0; + while (length > 0) + { + int bit = this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return -1; + } + + n = (n << 1) | bit; + length--; + } + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReceiveAndExtend(int length, Stream stream) + { + if (length == 1) + { + return this.ReadBit(stream) == 1 ? 1 : -1; + } + + int n = this.Receive(length, stream); + if (n >= 1 << (length - 1)) + { + return n; + } + + return n + (-1 << length) + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBaseline(ref FrameComponent component, int offset, ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, Stream stream) + { + int t = this.DecodeHuffman(ref dcHuffmanTable, stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream); + component.BlockData[offset] = (short)(component.Pred += diff); + + int k = 1; + while (k < 64) + { + int rs = this.DecodeHuffman(ref acHuffmanTable, stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + int s = rs & 15; + int r = rs >> 4; + + if (s == 0) + { + if (r < 15) + { + break; + } + + k += 16; + continue; + } + + k += r; + + if (k > 63) + { + break; + } + + byte z = QuantizationTables.DctZigZag[k]; + short re = (short)this.ReceiveAndExtend(s, stream); + component.BlockData[offset + z] = re; + k++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeDCFirst(ref FrameComponent component, int offset, ref HuffmanTable dcHuffmanTable, Stream stream) + { + int t = this.DecodeHuffman(ref dcHuffmanTable, stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream) << this.successiveState; + component.BlockData[offset] = (short)(component.Pred += diff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeDCSuccessive(ref FrameComponent component, int offset, Stream stream) + { + int bit = this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + component.BlockData[offset] |= (short)(bit << this.successiveState); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeACFirst(ref FrameComponent component, int offset, ref HuffmanTable acHuffmanTable, Stream stream) + { + if (this.eobrun > 0) + { + this.eobrun--; + return; + } + + Span componentBlockDataSpan = component.BlockData.Span; + int k = this.specStart; + int e = this.specEnd; + while (k <= e) + { + short rs = this.DecodeHuffman(ref acHuffmanTable, stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + int s = rs & 15; + int r = rs >> 4; + + if (s == 0) + { + if (r < 15) + { + this.eobrun = this.Receive(r, stream) + (1 << r) - 1; + break; + } + + k += 16; + continue; + } + + k += r; + byte z = QuantizationTables.DctZigZag[k]; + componentBlockDataSpan[offset + z] = (short)(this.ReceiveAndExtend(s, stream) * (1 << this.successiveState)); + k++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeACSuccessive(ref FrameComponent component, int offset, ref HuffmanTable acHuffmanTable, Stream stream) + { + int k = this.specStart; + int e = this.specEnd; + int r = 0; + Span componentBlockDataSpan = component.BlockData.Span; + while (k <= e) + { + byte z = QuantizationTables.DctZigZag[k]; + switch (this.successiveACState) + { + case 0: // Initial state + short rs = this.DecodeHuffman(ref acHuffmanTable, stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + int s = rs & 15; + r = rs >> 4; + if (s == 0) + { + if (r < 15) + { + this.eobrun = this.Receive(r, stream) + (1 << r); + this.successiveACState = 4; + } + else + { + r = 16; + this.successiveACState = 1; + } + } + else + { + if (s != 1) + { + throw new ImageFormatException("Invalid ACn encoding"); + } + + this.successiveACNextValue = this.ReceiveAndExtend(s, stream); + this.successiveACState = r > 0 ? 2 : 3; + } + + continue; + case 1: // Skipping r zero items + case 2: + if (componentBlockDataSpan[offset + z] != 0) + { + int bit = this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + componentBlockDataSpan[offset + z] += (short)(bit << this.successiveState); + } + else + { + r--; + if (r == 0) + { + this.successiveACState = this.successiveACState == 2 ? 3 : 0; + } + } + + break; + case 3: // Set value for a zero item + if (componentBlockDataSpan[offset + z] != 0) + { + int bit = this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + componentBlockDataSpan[offset + z] += (short)(bit << this.successiveState); + } + else + { + componentBlockDataSpan[offset + z] = (short)(this.successiveACNextValue << this.successiveState); + this.successiveACState = 0; + } + + break; + case 4: // Eob + if (componentBlockDataSpan[offset + z] != 0) + { + int bit = this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return; + } + + componentBlockDataSpan[offset + z] += (short)(bit << this.successiveState); + } + + break; + } + + k++; + } + + if (this.successiveACState == 4) + { + this.eobrun--; + if (this.eobrun == 0) + { + this.successiveACState = 0; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs new file mode 100644 index 000000000..02397e1d7 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs @@ -0,0 +1,128 @@ +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System.Runtime.CompilerServices; + using ImageSharp.PixelFormats; + + /// + /// Provides 8-bit lookup tables for converting from YCbCr to Rgb colorspace. + /// Methods to build the tables are based on libjpeg implementation. + /// + internal struct YCbCrToRgbTables + { + /// + /// The red red-chrominance table + /// + public static int[] CrRTable = new int[256]; + + /// + /// The blue blue-chrominance table + /// + public static int[] CbBTable = new int[256]; + + /// + /// The green red-chrominance table + /// + public static int[] CrGTable = new int[256]; + + /// + /// The green blue-chrominance table + /// + public static int[] CbGTable = new int[256]; + + // Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places. + private const int ScaleBits = 16; + + private const int Half = 1 << (ScaleBits - 1); + + private const int MinSample = 0; + + private const int HalfSample = 128; + + private const int MaxSample = 255; + + /// + /// Initializes the YCbCr tables + /// + public static void Create() + { + for (int i = 0, x = -128; i <= 255; i++, x++) + { + // i is the actual input pixel value, in the range 0..255 + // The Cb or Cr value we are thinking of is x = i - 128 + // Cr=>R value is nearest int to 1.402 * x + CrRTable[i] = RightShift((Fix(1.402F) * x) + Half); + + // Cb=>B value is nearest int to 1.772 * x + CbBTable[i] = RightShift((Fix(1.772F) * x) + Half); + + // Cr=>G value is scaled-up -0.714136286 + CrGTable[i] = (-Fix(0.714136286F)) * x; + + // Cb => G value is scaled - up - 0.344136286 * x + // We also add in Half so that need not do it in inner loop + CbGTable[i] = ((-Fix(0.344136286F)) * x) + Half; + } + } + + /// + /// Optimized method to pack bytes to the image from the YCbCr color space. + /// + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void PackYCbCr(ref TPixel packed, byte y, byte cb, byte cr) + where TPixel : struct, IPixel + { + byte r = (byte)(y + CrRTable[cr]).Clamp(0, 255); + + // The values for the G calculation are left scaled up, since we must add them together before rounding. + byte g = (byte)(y + RightShift(CbGTable[cb] + CrGTable[cr])).Clamp(0, 255); + + byte b = (byte)(y + CbBTable[cb]).Clamp(0, 255); + + packed.PackFromRgba32(new Rgba32(r, g, b, 255)); + } + + /// + /// Optimized method to pack bytes to the image from the YccK color space. + /// + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + /// The keyline component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void PackYccK(ref TPixel packed, byte y, byte cb, byte cr, byte k) + where TPixel : struct, IPixel + { + int c = (MaxSample - (y + CrRTable[cr])).Clamp(0, 255); + + // The values for the G calculation are left scaled up, since we must add them together before rounding. + int m = (MaxSample - (y + RightShift(CbGTable[cb] + CrGTable[cr]))).Clamp(0, 255); + + int cy = (MaxSample - (y + CbBTable[cb])).Clamp(0, 255); + + byte r = (byte)((c * k) / MaxSample); + byte g = (byte)((m * k) / MaxSample); + byte b = (byte)((cy * k) / MaxSample); + + packed.PackFromRgba32(new Rgba32(r, g, b, MaxSample)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Fix(float x) + { + return (int)((x * (1L << ScaleBits)) + 0.5F); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RightShift(int x) + { + return x >> ScaleBits; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs new file mode 100644 index 000000000..a02e05591 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -0,0 +1,359 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming +namespace ImageSharp.Formats.Jpeg.Port +{ + /// + /// Contains jpeg constant values + /// + internal static class JpegConstants + { + /// + /// Contains marker specific constants + /// + public static class Markers + { + /// + /// The prefix used for all markers. + /// + public const byte Prefix = 0xFF; + + /// + /// The Start of Image marker + /// + public const ushort SOI = 0xFFD8; + + /// + /// The End of Image marker + /// + public const ushort EOI = 0xFFD9; + + /// + /// Application specific marker for marking the jpeg format. + /// + /// + public const ushort APP0 = 0xFFE0; + + /// + /// Application specific marker for marking where to store metadata. + /// + public const ushort APP1 = 0xFFE1; + + /// + /// Application specific marker for marking where to store ICC profile information. + /// + public const ushort APP2 = 0xFFE2; + + /// + /// Application specific marker. + /// + public const ushort APP3 = 0xFFE3; + + /// + /// Application specific marker. + /// + public const ushort APP4 = 0xFFE4; + + /// + /// Application specific marker. + /// + public const ushort APP5 = 0xFFE5; + + /// + /// Application specific marker. + /// + public const ushort APP6 = 0xFFE6; + + /// + /// Application specific marker. + /// + public const ushort APP7 = 0xFFE7; + + /// + /// Application specific marker. + /// + public const ushort APP8 = 0xFFE8; + + /// + /// Application specific marker. + /// + public const ushort APP9 = 0xFFE9; + + /// + /// Application specific marker. + /// + public const ushort APP10 = 0xFFEA; + + /// + /// Application specific marker. + /// + public const ushort APP11 = 0xFFEB; + + /// + /// Application specific marker. + /// + public const ushort APP12 = 0xFFEC; + + /// + /// Application specific marker. + /// + public const ushort APP13 = 0xFFED; + + /// + /// Application specific marker used by Adobe for storing encoding information for DCT filters. + /// + public const ushort APP14 = 0xFFEE; + + /// + /// Application specific marker used by GraphicConverter to store JPEG quality. + /// + public const ushort APP15 = 0xFFEF; + + /// + /// The text comment marker + /// + public const ushort COM = 0xFFFE; + + /// + /// Define Quantization Table(s) marker + /// + /// Specifies one or more quantization tables. + /// + /// + public const ushort DQT = 0xFFDB; + + /// + /// Start of Frame (baseline DCT) + /// + /// Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const ushort SOF0 = 0xFFC0; + + /// + /// Start Of Frame (Extended Sequential DCT) + /// + /// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const ushort SOF1 = 0xFFC1; + + /// + /// Start Of Frame (progressive DCT) + /// + /// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const ushort SOF2 = 0xFFC2; + + /// + /// Define Huffman Table(s) + /// + /// Specifies one or more Huffman tables. + /// + /// + public const ushort DHT = 0xFFC4; + + /// + /// Define Restart Interval + /// + /// Specifies the interval between RSTn markers, in macroblocks.This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment. + /// + /// + public const ushort DRI = 0xFFDD; + + /// + /// Start of Scan + /// + /// Begins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. + /// Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it + /// will contain, and is immediately followed by entropy-coded data. + /// + /// + public const ushort SOS = 0xFFDA; + + /// + /// Define First Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const ushort RST0 = 0xFFD0; + + /// + /// Define Eigth Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const ushort RST7 = 0xFFD7; + + /// + /// Contains JFIF specific markers + /// + public static class JFif + { + /// + /// Represents J in ASCII + /// + public const byte J = 0x4A; + + /// + /// Represents F in ASCII + /// + public const byte F = 0x46; + + /// + /// Represents I in ASCII + /// + public const byte I = 0x49; + + /// + /// Represents the null "0" marker + /// + public const byte Null = 0x0; + } + + /// + /// Contains Adobe specific markers + /// + public static class Adobe + { + /// + /// Represents A in ASCII + /// + public const byte A = 0x41; + + /// + /// Represents d in ASCII + /// + public const byte D = 0x64; + + /// + /// Represents b in ASCII + /// + public const byte O = 0x6F; + + /// + /// Represents b in ASCII + /// + public const byte B = 0x62; + + /// + /// Represents e in ASCII + /// + public const byte E = 0x65; + + /// + /// The color transform is unknown.(RGB or CMYK) + /// + public const byte ColorTransformUnknown = 0; + + /// + /// The color transform is YCbCr (luminance, red chroma, blue chroma) + /// + public const byte ColorTransformYCbCr = 1; + + /// + /// The color transform is YCCK (luminance, red chroma, blue chroma, keyline) + /// + public const byte ColorTransformYcck = 2; + } + + /// + /// Contains EXIF specific markers + /// + public static class Exif + { + /// + /// Represents E in ASCII + /// + public const byte E = 0x45; + + /// + /// Represents x in ASCII + /// + public const byte X = 0x78; + + /// + /// Represents i in ASCII + /// + public const byte I = 0x69; + + /// + /// Represents f in ASCII + /// + public const byte F = 0x66; + + /// + /// Represents the null "0" marker + /// + public const byte Null = 0x0; + } + + /// + /// Contains ICC specific markers + /// + public static class ICC + { + /// + /// Represents I in ASCII + /// + public const byte I = 0x49; + + /// + /// Represents C in ASCII + /// + public const byte C = 0x43; + + /// + /// Represents _ in ASCII + /// + public const byte UnderScore = 0x5F; + + /// + /// Represents P in ASCII + /// + public const byte P = 0x50; + + /// + /// Represents R in ASCII + /// + public const byte R = 0x52; + + /// + /// Represents O in ASCII + /// + public const byte O = 0x4F; + + /// + /// Represents F in ASCII + /// + public const byte F = 0x46; + + /// + /// Represents L in ASCII + /// + public const byte L = 0x4C; + + /// + /// Represents E in ASCII + /// + public const byte E = 0x45; + + /// + /// Represents the null "0" marker + /// + public const byte Null = 0x0; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs new file mode 100644 index 000000000..3a6e14f59 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -0,0 +1,968 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Runtime.CompilerServices; + + using ImageSharp.Common.Extensions; + using ImageSharp.Formats.Jpeg.Port.Components; + using ImageSharp.Memory; + using ImageSharp.PixelFormats; + + /// + /// Performs the jpeg decoding operation. + /// Ported from with additional fixes to handle common encoding errors + /// + internal sealed class JpegDecoderCore : IDisposable + { + /// + /// The global configuration + /// + private readonly Configuration configuration; + + /// + /// Gets the temporary buffer used to store bytes read from the stream. + /// + private readonly byte[] temp = new byte[2 * 16 * 4]; + + private readonly byte[] markerBuffer = new byte[2]; + + private QuantizationTables quantizationTables; + + private HuffmanTables dcHuffmanTables; + + private HuffmanTables acHuffmanTables; + + private Frame frame; + + private ComponentBlocks components; + + private JpegPixelArea pixelArea; + + private ushort resetInterval; + + private int imageWidth; + + private int imageHeight; + + private int numberOfComponents; + + /// + /// Whether the image has a EXIF header + /// + private bool isExif; + + /// + /// Contains information about the JFIF marker + /// + private JFif jFif; + + /// + /// Contains information about the Adobe marker + /// + private Adobe adobe; + + /// + /// Initializes static members of the class. + /// + static JpegDecoderCore() + { + YCbCrToRgbTables.Create(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The options. + public JpegDecoderCore(Configuration configuration, IJpegDecoderOptions options) + { + this.configuration = configuration ?? Configuration.Default; + this.IgnoreMetadata = options.IgnoreMetadata; + } + + /// + /// Gets the input stream. + /// + public Stream InputStream { get; private set; } + + /// + /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + public bool IgnoreMetadata { get; } + + /// + /// Finds the next file marker within the byte stream. + /// + /// The buffer to read file markers to + /// The input stream + /// The + public static FileMarker FindNextFileMarker(byte[] marker, Stream stream) + { + int value = stream.Read(marker, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length - 2); + } + + if (marker[0] == JpegConstants.Markers.Prefix) + { + // According to Section B.1.1.2: + // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." + while (marker[1] == JpegConstants.Markers.Prefix) + { + int suffix = stream.ReadByte(); + if (suffix == -1) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length - 2); + } + + marker[1] = (byte)value; + } + + return new FileMarker((ushort)((marker[0] << 8) | marker[1]), (int)(stream.Position - 2)); + } + + return new FileMarker((ushort)((marker[0] << 8) | marker[1]), (int)(stream.Position - 2), true); + } + + /// + /// Decodes the image from the specified and sets the data to image. + /// + /// The pixel format. + /// The stream, where the image should be. + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : struct, IPixel + { + this.InputStream = stream; + + var metadata = new ImageMetaData(); + this.ParseStream(metadata, false); + + var image = new Image(this.configuration, this.imageWidth, this.imageHeight, metadata); + this.FillPixelData(image); + this.AssignResolution(image); + return image; + } + + /// + public void Dispose() + { + this.frame?.Dispose(); + this.components?.Dispose(); + this.quantizationTables?.Dispose(); + this.dcHuffmanTables?.Dispose(); + this.acHuffmanTables?.Dispose(); + this.pixelArea.Dispose(); + + // Set large fields to null. + this.frame = null; + this.components = null; + this.quantizationTables = null; + this.dcHuffmanTables = null; + this.acHuffmanTables = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockBufferOffset(ref Component component, int row, int col) + { + return 64 * (((component.BlocksPerLine + 1) * row) + col); + } + + /// + /// Parses the input stream for file markers + /// + /// Contains the metadata for an image + /// Whether to decode metadata only. + private void ParseStream(ImageMetaData metaData, bool metadataOnly) + { + // TODO: metadata only logic + // Check for the Start Of Image marker. + var fileMarker = new FileMarker(this.ReadUint16(), 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) + { + throw new ImageFormatException("Missing SOI marker."); + } + + ushort marker = this.ReadUint16(); + fileMarker = new FileMarker(marker, (int)this.InputStream.Position - 2); + + this.quantizationTables = new QuantizationTables(); + this.dcHuffmanTables = new HuffmanTables(); + this.acHuffmanTables = new HuffmanTables(); + + while (fileMarker.Marker != JpegConstants.Markers.EOI) + { + // Get the marker length + int remaining = this.ReadUint16() - 2; + + switch (fileMarker.Marker) + { + case JpegConstants.Markers.APP0: + this.ProcessApplicationHeaderMarker(remaining); + break; + + case JpegConstants.Markers.APP1: + this.ProcessApp1Marker(remaining, metaData); + break; + + case JpegConstants.Markers.APP2: + this.ProcessApp2Marker(remaining, metaData); + break; + case JpegConstants.Markers.APP3: + case JpegConstants.Markers.APP4: + case JpegConstants.Markers.APP5: + case JpegConstants.Markers.APP6: + case JpegConstants.Markers.APP7: + case JpegConstants.Markers.APP8: + case JpegConstants.Markers.APP9: + case JpegConstants.Markers.APP10: + case JpegConstants.Markers.APP11: + case JpegConstants.Markers.APP12: + case JpegConstants.Markers.APP13: + this.InputStream.Skip(remaining); + break; + + case JpegConstants.Markers.APP14: + this.ProcessApp14Marker(remaining); + break; + + case JpegConstants.Markers.APP15: + case JpegConstants.Markers.COM: + this.InputStream.Skip(remaining); + break; + + case JpegConstants.Markers.DQT: + this.ProcessDefineQuantizationTablesMarker(remaining); + break; + + case JpegConstants.Markers.SOF0: + case JpegConstants.Markers.SOF1: + case JpegConstants.Markers.SOF2: + this.ProcessStartOfFrameMarker(remaining, fileMarker); + break; + + case JpegConstants.Markers.DHT: + this.ProcessDefineHuffmanTablesMarker(remaining); + break; + + case JpegConstants.Markers.DRI: + this.ProcessDefineRestartIntervalMarker(remaining); + break; + + case JpegConstants.Markers.SOS: + this.ProcessStartOfScanMarker(); + break; + } + + // Read on. + fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream); + } + + this.imageWidth = this.frame.SamplesPerLine; + this.imageHeight = this.frame.Scanlines; + this.components = new ComponentBlocks { Components = new Component[this.frame.ComponentCount] }; + + for (int i = 0; i < this.components.Components.Length; i++) + { + ref var frameComponent = ref this.frame.Components[i]; + var component = new Component + { + Scale = new System.Numerics.Vector2( + frameComponent.HorizontalFactor / (float)this.frame.MaxHorizontalFactor, + frameComponent.VerticalFactor / (float)this.frame.MaxVerticalFactor), + BlocksPerLine = frameComponent.BlocksPerLine, + BlocksPerColumn = frameComponent.BlocksPerColumn + }; + + this.BuildComponentData(ref component, ref frameComponent); + this.components.Components[i] = component; + } + + this.numberOfComponents = this.components.Components.Length; + } + + /// + /// Fills the given image with the color data + /// + /// The pixel format. + /// The image + private void FillPixelData(Image image) + where TPixel : struct, IPixel + { + if (this.numberOfComponents > 4) + { + throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numberOfComponents}"); + } + + this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numberOfComponents); + this.pixelArea.LinearizeBlockData(this.components, image.Width, image.Height); + + if (this.numberOfComponents == 1) + { + this.FillGrayScaleImage(image); + return; + } + + if (this.numberOfComponents == 3) + { + if (this.adobe.Equals(default(Adobe)) || this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYCbCr) + { + this.FillYCbCrImage(image); + } + else if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformUnknown) + { + this.FillRgbImage(image); + } + } + + if (this.numberOfComponents == 4) + { + if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYcck) + { + this.FillYcckImage(image); + } + else + { + this.FillCmykImage(image); + } + } + } + + /// + /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header or EXIF metadata. + /// + /// The pixel format. + /// The image to assign the resolution to. + private void AssignResolution(Image image) + where TPixel : struct, IPixel + { + if (this.isExif) + { + ExifValue horizontal = image.MetaData.ExifProfile.GetValue(ExifTag.XResolution); + ExifValue vertical = image.MetaData.ExifProfile.GetValue(ExifTag.YResolution); + double horizontalValue = horizontal != null ? ((Rational)horizontal.Value).ToDouble() : 0; + double verticalValue = vertical != null ? ((Rational)vertical.Value).ToDouble() : 0; + + if (horizontalValue > 0 && verticalValue > 0) + { + image.MetaData.HorizontalResolution = horizontalValue; + image.MetaData.VerticalResolution = verticalValue; + } + } + else if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0) + { + image.MetaData.HorizontalResolution = this.jFif.XDensity; + image.MetaData.VerticalResolution = this.jFif.YDensity; + } + } + + /// + /// Processes the application header containing the JFIF identifier plus extra data. + /// + /// The remaining bytes in the segment block. + private void ProcessApplicationHeaderMarker(int remaining) + { + if (remaining < 5) + { + // Skip the application header length + this.InputStream.Skip(remaining); + return; + } + + this.InputStream.Read(this.temp, 0, 13); + remaining -= 13; + + bool isJfif = this.temp[0] == JpegConstants.Markers.JFif.J && + this.temp[1] == JpegConstants.Markers.JFif.F && + this.temp[2] == JpegConstants.Markers.JFif.I && + this.temp[3] == JpegConstants.Markers.JFif.F && + this.temp[4] == JpegConstants.Markers.JFif.Null; + + if (isJfif) + { + this.jFif = new JFif + { + MajorVersion = this.temp[5], + MinorVersion = this.temp[6], + DensityUnits = this.temp[7], + XDensity = (short)((this.temp[8] << 8) | this.temp[9]), + YDensity = (short)((this.temp[10] << 8) | this.temp[11]) + }; + } + + // TODO: thumbnail + if (remaining > 0) + { + this.InputStream.Skip(remaining); + } + } + + /// + /// Processes the App1 marker retrieving any stored metadata + /// + /// The remaining bytes in the segment block. + /// The image. + private void ProcessApp1Marker(int remaining, ImageMetaData metadata) + { + if (remaining < 6 || this.IgnoreMetadata) + { + // Skip the application header length + this.InputStream.Skip(remaining); + return; + } + + byte[] profile = new byte[remaining]; + this.InputStream.Read(profile, 0, remaining); + + if (profile[0] == JpegConstants.Markers.Exif.E && + profile[1] == JpegConstants.Markers.Exif.X && + profile[2] == JpegConstants.Markers.Exif.I && + profile[3] == JpegConstants.Markers.Exif.F && + profile[4] == JpegConstants.Markers.Exif.Null && + profile[5] == JpegConstants.Markers.Exif.Null) + { + this.isExif = true; + metadata.ExifProfile = new ExifProfile(profile); + } + } + + /// + /// Processes the App2 marker retrieving any stored ICC profile information + /// + /// The remaining bytes in the segment block. + /// The image. + private void ProcessApp2Marker(int remaining, ImageMetaData metadata) + { + // Length is 14 though we only need to check 12. + const int Icclength = 14; + if (remaining < Icclength || this.IgnoreMetadata) + { + this.InputStream.Skip(remaining); + return; + } + + byte[] identifier = new byte[Icclength]; + this.InputStream.Read(identifier, 0, Icclength); + remaining -= Icclength; // We have read it by this point + + if (identifier[0] == JpegConstants.Markers.ICC.I && + identifier[1] == JpegConstants.Markers.ICC.C && + identifier[2] == JpegConstants.Markers.ICC.C && + identifier[3] == JpegConstants.Markers.ICC.UnderScore && + identifier[4] == JpegConstants.Markers.ICC.P && + identifier[5] == JpegConstants.Markers.ICC.R && + identifier[6] == JpegConstants.Markers.ICC.O && + identifier[7] == JpegConstants.Markers.ICC.F && + identifier[8] == JpegConstants.Markers.ICC.I && + identifier[9] == JpegConstants.Markers.ICC.L && + identifier[10] == JpegConstants.Markers.ICC.E && + identifier[11] == JpegConstants.Markers.ICC.Null) + { + byte[] profile = new byte[remaining]; + this.InputStream.Read(profile, 0, remaining); + + if (metadata.IccProfile == null) + { + metadata.IccProfile = new IccProfile(profile); + } + else + { + metadata.IccProfile.Extend(profile); + } + } + else + { + // Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this. + this.InputStream.Skip(remaining); + } + } + + /// + /// Processes the application header containing the Adobe identifier + /// which stores image encoding information for DCT filters. + /// + /// The remaining bytes in the segment block. + private void ProcessApp14Marker(int remaining) + { + if (remaining < 12) + { + // Skip the application header length + this.InputStream.Skip(remaining); + return; + } + + this.InputStream.Read(this.temp, 0, 12); + remaining -= 12; + + bool isAdobe = this.temp[0] == JpegConstants.Markers.Adobe.A && + this.temp[1] == JpegConstants.Markers.Adobe.D && + this.temp[2] == JpegConstants.Markers.Adobe.O && + this.temp[3] == JpegConstants.Markers.Adobe.B && + this.temp[4] == JpegConstants.Markers.Adobe.E; + + if (isAdobe) + { + this.adobe = new Adobe + { + DCTEncodeVersion = (short)((this.temp[5] << 8) | this.temp[6]), + APP14Flags0 = (short)((this.temp[7] << 8) | this.temp[8]), + APP14Flags1 = (short)((this.temp[9] << 8) | this.temp[10]), + ColorTransform = this.temp[11] + }; + } + + if (remaining > 0) + { + this.InputStream.Skip(remaining); + } + } + + /// + /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. + /// + /// The remaining bytes in the segment block. + /// + /// Thrown if the tables do not match the header + /// + private void ProcessDefineQuantizationTablesMarker(int remaining) + { + while (remaining > 0) + { + bool done = false; + remaining--; + int quantizationTableSpec = this.InputStream.ReadByte(); + + switch (quantizationTableSpec >> 4) + { + case 0: + { + // 8 bit values + if (remaining < 64) + { + done = true; + break; + } + + this.InputStream.Read(this.temp, 0, 64); + remaining -= 64; + + Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec & 15); + for (int j = 0; j < 64; j++) + { + tableSpan[QuantizationTables.DctZigZag[j]] = this.temp[j]; + } + } + + break; + case 1: + { + // 16 bit values + if (remaining < 128) + { + done = true; + break; + } + + this.InputStream.Read(this.temp, 0, 128); + remaining -= 128; + + Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec & 15); + for (int j = 0; j < 64; j++) + { + tableSpan[QuantizationTables.DctZigZag[j]] = (short)((this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]); + } + } + + break; + default: + throw new ImageFormatException("Bad Tq index value"); + } + + if (done) + { + break; + } + } + + if (remaining != 0) + { + throw new ImageFormatException("DQT has wrong length"); + } + } + + /// + /// Processes the Start of Frame marker. Specified in section B.2.2. + /// + /// The remaining bytes in the segment block. + /// The current frame marker. + private void ProcessStartOfFrameMarker(int remaining, FileMarker frameMarker) + { + if (this.frame != null) + { + throw new ImageFormatException("Multiple SOF markers. Only single frame jpegs supported."); + } + + this.InputStream.Read(this.temp, 0, remaining); + + this.frame = new Frame + { + Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, + Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2, + Precision = this.temp[0], + Scanlines = (short)((this.temp[1] << 8) | this.temp[2]), + SamplesPerLine = (short)((this.temp[3] << 8) | this.temp[4]), + ComponentCount = this.temp[5] + }; + + int maxH = 0; + int maxV = 0; + int index = 6; + + // No need to pool this. They max out at 4 + this.frame.ComponentIds = new byte[this.frame.ComponentCount]; + this.frame.Components = new FrameComponent[this.frame.ComponentCount]; + + for (int i = 0; i < this.frame.Components.Length; i++) + { + int h = this.temp[index + 1] >> 4; + int v = this.temp[index + 1] & 15; + + if (maxH < h) + { + maxH = h; + } + + if (maxV < v) + { + maxV = v; + } + + ref var component = ref this.frame.Components[i]; + component.Id = this.temp[index]; + component.HorizontalFactor = h; + component.VerticalFactor = v; + component.QuantizationIdentifier = this.temp[index + 2]; + + this.frame.ComponentIds[i] = component.Id; + + index += 3; + } + + this.frame.MaxHorizontalFactor = maxH; + this.frame.MaxVerticalFactor = maxV; + this.PrepareComponents(); + } + + /// + /// Processes a Define Huffman Table marker, and initializes a huffman + /// struct from its contents. Specified in section B.2.4.2. + /// + /// The remaining bytes in the segment block. + private void ProcessDefineHuffmanTablesMarker(int remaining) + { + if (remaining < 17) + { + throw new ImageFormatException($"DHT has wrong length: {remaining}"); + } + + using (var huffmanData = Buffer.CreateClean(256)) + { + for (int i = 2; i < remaining;) + { + byte huffmanTableSpec = (byte)this.InputStream.ReadByte(); + this.InputStream.Read(huffmanData.Array, 0, 16); + + using (var codeLengths = Buffer.CreateClean(17)) + { + int codeLengthSum = 0; + + for (int j = 1; j < 17; j++) + { + codeLengthSum += codeLengths[j] = huffmanData[j - 1]; + } + + using (var huffmanValues = Buffer.CreateClean(256)) + { + this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); + + i += 17 + codeLengthSum; + + this.BuildHuffmanTable( + huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, + huffmanTableSpec & 15, + codeLengths.Array, + huffmanValues.Array); + } + } + } + } + } + + /// + /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in + /// macroblocks + /// + /// The remaining bytes in the segment block. + private void ProcessDefineRestartIntervalMarker(int remaining) + { + if (remaining != 2) + { + throw new ImageFormatException($"DRI has wrong length: {remaining}"); + } + + this.resetInterval = this.ReadUint16(); + } + + /// + /// Processes the SOS (Start of scan marker). + /// + private void ProcessStartOfScanMarker() + { + int selectorsCount = this.InputStream.ReadByte(); + int componentIndex = -1; + for (int i = 0; i < selectorsCount; i++) + { + componentIndex = -1; + int selector = this.InputStream.ReadByte(); + + for (int j = 0; j < this.frame.ComponentIds.Length; j++) + { + byte id = this.frame.ComponentIds[j]; + if (selector == id) + { + componentIndex = j; + } + } + + if (componentIndex < 0) + { + throw new ImageFormatException("Unknown component selector"); + } + + ref FrameComponent component = ref this.frame.Components[componentIndex]; + int tableSpec = this.InputStream.ReadByte(); + component.DCHuffmanTableId = tableSpec >> 4; + component.ACHuffmanTableId = tableSpec & 15; + } + + this.InputStream.Read(this.temp, 0, 3); + + int spectralStart = this.temp[0]; + int spectralEnd = this.temp[1]; + int successiveApproximation = this.temp[2]; + var scanDecoder = default(ScanDecoder); + + scanDecoder.DecodeScan( + this.frame, + this.InputStream, + this.dcHuffmanTables, + this.acHuffmanTables, + this.frame.Components, + componentIndex, + selectorsCount, + this.resetInterval, + spectralStart, + spectralEnd, + successiveApproximation >> 4, + successiveApproximation & 15); + } + + /// + /// Build the data for the given component + /// + /// The component + /// The frame component + private void BuildComponentData(ref Component component, ref FrameComponent frameComponent) + { + int blocksPerLine = component.BlocksPerLine; + int blocksPerColumn = component.BlocksPerColumn; + using (var computationBuffer = Buffer.CreateClean(64)) + using (var multiplicationBuffer = Buffer.CreateClean(64)) + { + Span quantizationTable = this.quantizationTables.Tables.GetRowSpan(frameComponent.QuantizationIdentifier); + Span computationBufferSpan = computationBuffer; + + // For AA&N IDCT method, multiplier are equal to quantization + // coefficients scaled by scalefactor[row]*scalefactor[col], where + // scalefactor[0] = 1 + // scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 + // For integer operation, the multiplier table is to be scaled by 12. + Span multiplierSpan = multiplicationBuffer; + + // for (int i = 0; i < 64; i++) + // { + // multiplierSpan[i] = (short)IDCT.Descale(quantizationTable[i] * IDCT.Aanscales[i], 12); + // } + for (int blockRow = 0; blockRow < blocksPerColumn; blockRow++) + { + for (int blockCol = 0; blockCol < blocksPerLine; blockCol++) + { + int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); + IDCT.QuantizeAndInverse(ref frameComponent, offset, ref computationBufferSpan, ref quantizationTable); + } + } + } + + component.Output = frameComponent.BlockData; + } + + /// + /// Builds the huffman tables + /// + /// The tables + /// The table index + /// The codelengths + /// The values + private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, byte[] values) + { + tables[index] = new HuffmanTable(codeLengths, values); + } + + /// + /// Allocates the frame component blocks + /// + private void PrepareComponents() + { + int mcusPerLine = (int)MathF.Ceiling(this.frame.SamplesPerLine / 8F / this.frame.MaxHorizontalFactor); + int mcusPerColumn = (int)MathF.Ceiling(this.frame.Scanlines / 8F / this.frame.MaxVerticalFactor); + + for (int i = 0; i < this.frame.ComponentCount; i++) + { + ref var component = ref this.frame.Components[i]; + int blocksPerLine = (int)MathF.Ceiling(MathF.Ceiling(this.frame.SamplesPerLine / 8F) * component.HorizontalFactor / this.frame.MaxHorizontalFactor); + int blocksPerColumn = (int)MathF.Ceiling(MathF.Ceiling(this.frame.Scanlines / 8F) * component.VerticalFactor / this.frame.MaxVerticalFactor); + int blocksPerLineForMcu = mcusPerLine * component.HorizontalFactor; + int blocksPerColumnForMcu = mcusPerColumn * component.VerticalFactor; + + int blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1); + + // Pooled. Disposed via frame disposal + component.BlockData = Buffer.CreateClean(blocksBufferSize); + component.BlocksPerLine = blocksPerLine; + component.BlocksPerColumn = blocksPerColumn; + } + + this.frame.McusPerLine = mcusPerLine; + this.frame.McusPerColumn = mcusPerColumn; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillGrayScaleImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + + for (int x = 0; x < image.Width; x++) + { + ref byte luminance = ref areaRowSpan[x]; + ref TPixel pixel = ref imageRowSpan[x]; + var rgba = new Rgba32(luminance, luminance, luminance); + pixel.PackFromRgba32(rgba); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillYCbCrImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + for (int x = 0, o = 0; x < image.Width; x++, o += 3) + { + ref byte yy = ref areaRowSpan[o]; + ref byte cb = ref areaRowSpan[o + 1]; + ref byte cr = ref areaRowSpan[o + 2]; + ref TPixel pixel = ref imageRowSpan[x]; + YCbCrToRgbTables.PackYCbCr(ref pixel, yy, cb, cr); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillYcckImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + for (int x = 0, o = 0; x < image.Width; x++, o += 4) + { + ref byte yy = ref areaRowSpan[o]; + ref byte cb = ref areaRowSpan[o + 1]; + ref byte cr = ref areaRowSpan[o + 2]; + ref byte k = ref areaRowSpan[o + 3]; + + ref TPixel pixel = ref imageRowSpan[x]; + YCbCrToRgbTables.PackYccK(ref pixel, yy, cb, cr, k); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillCmykImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + for (int x = 0, o = 0; x < image.Width; x++, o += 4) + { + ref byte c = ref areaRowSpan[o]; + ref byte m = ref areaRowSpan[o + 1]; + ref byte cy = ref areaRowSpan[o + 2]; + ref byte k = ref areaRowSpan[o + 3]; + + byte r = (byte)((c * k) / 255); + byte g = (byte)((m * k) / 255); + byte b = (byte)((cy * k) / 255); + + ref TPixel pixel = ref imageRowSpan[x]; + var rgba = new Rgba32(r, g, b); + pixel.PackFromRgba32(rgba); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillRgbImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + + PixelOperations.Instance.PackFromRgb24Bytes(areaRowSpan, imageRowSpan, image.Width); + } + } + + /// + /// Reads a from the stream advancing it by two bytes + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort ReadUint16() + { + this.InputStream.Read(this.markerBuffer, 0, 2); + return (ushort)((this.markerBuffer[0] << 8) | this.markerBuffer[1]); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Memory/Fast2DArray{T}.cs b/src/ImageSharp/Memory/Fast2DArray{T}.cs index 260c829e2..ee8a60983 100644 --- a/src/ImageSharp/Memory/Fast2DArray{T}.cs +++ b/src/ImageSharp/Memory/Fast2DArray{T}.cs @@ -106,11 +106,23 @@ namespace ImageSharp.Memory return new Fast2DArray(data); } + /// + /// Gets a representing the row beginning from the the first item on that row. + /// + /// The y-coordinate of the row. Must be greater than or equal to zero and less than the height of the 2D array. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetRowSpan(int row) + { + this.CheckCoordinates(row); + return new Span(this.Data, row * this.Width, this.Width); + } + /// /// Checks the coordinates to ensure they are within bounds. /// - /// The row-coordinate of the item. Must be greater than zero and smaller than the height of the array. - /// The column-coordinate of the item. Must be greater than zero and smaller than the width of the array. + /// The y-coordinate of the item. Must be greater than zero and smaller than the height of the array. + /// The x-coordinate of the item. Must be greater than zero and smaller than the width of the array. /// /// Thrown if the coordinates are not within the bounds of the array. /// @@ -127,5 +139,21 @@ namespace ImageSharp.Memory throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outwith the array bounds."); } } + + /// + /// Checks the coordinates to ensure they are within bounds. + /// + /// The y-coordinate of the item. Must be greater than zero and smaller than the height of the array. + /// + /// Thrown if the coordinates are not within the bounds of the image. + /// + [Conditional("DEBUG")] + private void CheckCoordinates(int row) + { + if (row < 0 || row >= this.Height) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outwith the array bounds."); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs index a7fd8fd6a..be4201c72 100644 --- a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs @@ -5,6 +5,7 @@ namespace ImageSharp { + using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -82,9 +83,11 @@ namespace ImageSharp this.values.Add(new ExifValue(value)); } } - else + + if (other.data != null) { - this.data = other.data; + this.data = new byte[other.data.Length]; + Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); } } @@ -131,7 +134,7 @@ namespace ImageSharp return null; } - if (this.data.Length < (this.thumbnailOffset + this.thumbnailLength)) + if (this.data == null || this.data.Length < (this.thumbnailOffset + this.thumbnailLength)) { return null; } diff --git a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs index 978d5bc24..1393afa62 100644 --- a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs @@ -59,7 +59,11 @@ namespace ImageSharp Guard.NotNull(other, nameof(other)); // TODO: Do we need to copy anything else? - this.data = other.data; + if (other.data != null) + { + this.data = new byte[other.data.Length]; + Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); + } } /// diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs deleted file mode 100644 index caadaaa34..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class DivFloat - { - private float[] input; - - private float[] result; - - [Params(32)] - public int InputSize { get; set; } - - private float testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new float[this.InputSize]; - this.result = new float[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = (uint)i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - float v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] / v; - } - } - - [Benchmark] - public void Simd() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a / v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs deleted file mode 100644 index 6b3edb192..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class DivUInt32 - { - private uint[] input; - - private uint[] result; - - [Params(32)] - public int InputSize { get; set; } - - private uint testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new uint[this.InputSize]; - this.result = new uint[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = (uint)i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - uint v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] / v; - } - } - - [Benchmark] - public void Simd() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a / v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs new file mode 100644 index 000000000..b38429557 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs @@ -0,0 +1,69 @@ +namespace ImageSharp.Benchmarks.General.Vectorization +{ + using System; + using System.Numerics; + + using BenchmarkDotNet.Attributes; + + public class DivFloat : SIMDBenchmarkBase.Divide + { + protected override float GetTestValue() => 42; + + [Benchmark(Baseline = true)] + public void Standard() + { + float v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] / v; + } + } + } + + public class Divide : SIMDBenchmarkBase.Divide + { + protected override uint GetTestValue() => 42; + + [Benchmark(Baseline = true)] + public void Standard() + { + uint v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] / v; + } + } + } + + public class DivInt32 : SIMDBenchmarkBase.Divide + { + protected override int GetTestValue() => 42; + + [Benchmark(Baseline = true)] + public void Standard() + { + int v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] / v; + } + } + } + + public class DivInt16 : SIMDBenchmarkBase.Divide + { + protected override short GetTestValue() => 42; + + protected override Vector GetTestVector() => new Vector(new short[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}); + + [Benchmark(Baseline = true)] + public void Standard() + { + short v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = (short)(this.input[i] / v); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs deleted file mode 100644 index e5ae49b2a..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class MulFloat - { - private float[] input; - - private float[] result; - - [Params(32)] - public int InputSize { get; set; } - - private float testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new float[this.InputSize]; - this.result = new float[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - float v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] * v; - } - } - - [Benchmark] - public void SimdMultiplyByVector() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a * v; - a.CopyTo(this.result, i); - } - } - - [Benchmark] - public void SimdMultiplyByScalar() - { - float v = this.testValue; - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a * v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs deleted file mode 100644 index 532acc02d..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class MulUInt32 - { - private uint[] input; - - private uint[] result; - - [Params(32)] - public int InputSize { get; set; } - - private uint testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new uint[this.InputSize]; - this.result = new uint[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = (uint)i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - uint v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] * v; - } - } - - [Benchmark] - public void Simd() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a * v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs new file mode 100644 index 000000000..d1b70f21b --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs @@ -0,0 +1,50 @@ +namespace ImageSharp.Benchmarks.General.Vectorization +{ + using System.Numerics; + using BenchmarkDotNet.Attributes; + + public class MulUInt32 : SIMDBenchmarkBase.Multiply + { + protected override uint GetTestValue() => 42u; + + [Benchmark(Baseline = true)] + public void Standard() + { + uint v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] * v; + } + } + } + + public class MulInt32 : SIMDBenchmarkBase.Multiply + { + [Benchmark(Baseline = true)] + public void Standard() + { + int v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] * v; + } + } + } + + public class MulInt16 : SIMDBenchmarkBase.Multiply + { + protected override short GetTestValue() => 42; + + protected override Vector GetTestVector() => new Vector(new short[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }); + + [Benchmark(Baseline = true)] + public void Standard() + { + short v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = (short)(this.input[i] * v); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs new file mode 100644 index 000000000..76987dbd2 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs @@ -0,0 +1,70 @@ +namespace ImageSharp.Benchmarks.General.Vectorization +{ + using System.Numerics; + using System.Runtime.CompilerServices; + + using BenchmarkDotNet.Attributes; + + public abstract class SIMDBenchmarkBase + where T : struct + { + protected T[] input; + + protected T[] result; + + protected T testValue; + + protected Vector testVector; + + protected virtual T GetTestValue() => default(T); + + protected virtual Vector GetTestVector() => new Vector(this.GetTestValue()); + + + [Params(32)] + public int InputSize { get; set; } + + [GlobalSetup] + public virtual void Setup() + { + this.input = new T[this.InputSize]; + this.result = new T[this.InputSize]; + this.testValue = this.GetTestValue(); + this.testVector = this.GetTestVector(); + } + + public abstract class Multiply : SIMDBenchmarkBase + { + [Benchmark] + public void Simd() + { + Vector v = this.testVector; + + for (int i = 0; i < this.input.Length; i += Vector.Count) + { + Vector a = Unsafe.As>(ref this.input[i]); + a = a * v; + Unsafe.As>(ref this.result[i]) = a; + } + } + } + + public abstract class Divide : SIMDBenchmarkBase + { + [Benchmark] + public void Simd() + { + Vector v = this.testVector; + + for (int i = 0; i < this.input.Length; i += Vector.Count) + { + Vector a = Unsafe.As>(ref this.input[i]); + a = a / v; + Unsafe.As>(ref this.result[i]) = a; + } + } + } + + + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs index ece93f912..aaf44c692 100644 --- a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs @@ -14,6 +14,7 @@ namespace ImageSharp.Benchmarks.Image using CoreSize = SixLabors.Primitives.Size; + [Config(typeof(Config))] public class DecodeJpeg : BenchmarkBase { private byte[] jpegBytes; @@ -23,21 +24,21 @@ namespace ImageSharp.Benchmarks.Image { if (this.jpegBytes == null) { - this.jpegBytes = File.ReadAllBytes("../ImageSharp.Tests/TestImages/Formats/Jpg/Baseline/Calliphora.jpg"); + this.jpegBytes = File.ReadAllBytes("../../../../../../../../ImageSharp.Tests/TestImages/Formats/Jpg/Baseline/Calliphora.jpg"); } } - [Benchmark(Baseline = true, Description = "System.Drawing Jpeg")] - public Size JpegSystemDrawing() - { - using (MemoryStream memoryStream = new MemoryStream(this.jpegBytes)) - { - using (Image image = Image.FromStream(memoryStream)) - { - return image.Size; - } - } - } + //[Benchmark(Baseline = true, Description = "System.Drawing Jpeg")] + //public Size JpegSystemDrawing() + //{ + // using (MemoryStream memoryStream = new MemoryStream(this.jpegBytes)) + // { + // using (Image image = Image.FromStream(memoryStream)) + // { + // return image.Size; + // } + // } + //} [Benchmark(Description = "ImageSharp Jpeg")] public CoreSize JpegCore() diff --git a/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs b/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs index 44c90d253..ebcdf972a 100644 --- a/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs +++ b/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs @@ -6,8 +6,13 @@ namespace ImageSharp.Benchmarks.Image { using System.Collections.Generic; + using System.IO; + using BenchmarkDotNet.Attributes; + using ImageSharp.Formats; + using ImageSharp.PixelFormats; + using CoreImage = ImageSharp.Image; [Config(typeof(Config.Short))] @@ -20,14 +25,23 @@ namespace ImageSharp.Benchmarks.Image protected override IEnumerable SearchPatterns => new[] { "*.jpg" }; - [Benchmark(Description = "DecodeJpegMultiple - ImageSharp")] - public void DecodeJpegImageSharp() + [Benchmark(Description = "DecodeJpegMultiple - ImageSharp NEW")] + public void DecodeJpegImageSharpNwq() { this.ForEachStream( ms => CoreImage.Load(ms) ); } + + [Benchmark(Description = "DecodeJpegMultiple - ImageSharp Original")] + public void DecodeJpegImageSharpOriginal() + { + this.ForEachStream( + ms => CoreImage.Load(ms, new OriginalJpegDecoder()) + ); + } + [Benchmark(Baseline = true, Description = "DecodeJpegMultiple - System.Drawing")] public void DecodeJpegSystemDrawing() { @@ -36,5 +50,25 @@ namespace ImageSharp.Benchmarks.Image ); } + + public sealed class OriginalJpegDecoder : IImageDecoder, IJpegDecoderOptions + { + /// + /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + public bool IgnoreMetadata { get; set; } + + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : struct, IPixel + { + Guard.NotNull(stream, "stream"); + + using (var decoder = new JpegDecoderCore(configuration, this)) + { + return decoder.Decode(stream); + } + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs b/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs index e01a5951e..fcd72a9ba 100644 --- a/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs +++ b/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs @@ -39,7 +39,7 @@ namespace ImageSharp.Benchmarks.Image [Params(InputImageCategory.AllImages, InputImageCategory.SmallImagesOnly, InputImageCategory.LargeImagesOnly)] public virtual InputImageCategory InputCategory { get; set; } - protected virtual string BaseFolder => "../ImageSharp.Tests/TestImages/Formats/"; + protected virtual string BaseFolder => "../../../../../../../../ImageSharp.Tests/TestImages/Formats/"; protected virtual IEnumerable SearchPatterns => new[] { "*.*" }; diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index b536af71f..8b221f75b 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -40,10 +40,10 @@ namespace ImageSharp.Sandbox46 /// public static void Main(string[] args) { - // RunDecodeJpegProfilingTests(); + RunDecodeJpegProfilingTests(); // RunToVector4ProfilingTest(); - RunResizeProfilingTest(); + //RunResizeProfilingTest(); Console.ReadLine(); } diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs index 68ccfe06b..dea780cd7 100644 --- a/tests/ImageSharp.Tests/FileTestBase.cs +++ b/tests/ImageSharp.Tests/FileTestBase.cs @@ -75,7 +75,9 @@ namespace ImageSharp.Tests // TestFile.Create(TestImages.Jpeg.Baseline.Ycck), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.Cmyk), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.Floorplan), // Perf: Enable for local testing only + // TestFile.Create(TestImages.Jpeg.Progressive.Festzug), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.Bad.MissingEOF), // Perf: Enable for local testing only + // TestFile.Create(TestImages.Jpeg.Baseline.Bad.ExifUndefType), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Progressive.Fb), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Progressive.Progress), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.GammaDalaiLamaGray), // Perf: Enable for local testing only diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/baseline/ycck - Copy.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/baseline/ycck - Copy.jpg new file mode 100644 index 000000000..30e88773e Binary files /dev/null and b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/baseline/ycck - Copy.jpg differ diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm new file mode 100644 index 000000000..72a5e448b --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm @@ -0,0 +1,63 @@ + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js new file mode 100644 index 000000000..6ebf71a69 --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js @@ -0,0 +1,1205 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable no-multi-spaces */ + +// import { error, warn } from '../shared/util'; + +/** + * This code was forked from https://github.com/notmasteryet/jpgjs. + * The original version was created by GitHub user notmasteryet. + * + * - The JPEG specification can be found in the ITU CCITT Recommendation T.81 + * (www.w3.org/Graphics/JPEG/itu-t81.pdf) + * - The JFIF specification can be found in the JPEG File Interchange Format + * (www.w3.org/Graphics/JPEG/jfif3.pdf) + * - The Adobe Application-Specific JPEG markers in the + * Supporting the DCT Filters in PostScript Level 2, Technical Note #5116 + * (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf) + */ + +var error = function(val){ + console.log(val); +} + +var warn = function(val){ + console.log(val); +} + +var JpegImage = (function JpegImageClosure() { + var dctZigZag = new Uint8Array([ + 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 + ]); + + var dctCos1 = 4017; // cos(pi/16) + var dctSin1 = 799; // sin(pi/16) + var dctCos3 = 3406; // cos(3*pi/16) + var dctSin3 = 2276; // sin(3*pi/16) + var dctCos6 = 1567; // cos(6*pi/16) + var dctSin6 = 3784; // sin(6*pi/16) + var dctSqrt2 = 5793; // sqrt(2) + var dctSqrt1d2 = 2896; // sqrt(2) / 2 + + function JpegImage() { + this.decodeTransform = null; + this.colorTransform = -1; + } + + function buildHuffmanTable(codeLengths, values) { + console.log(codeLengths); + console.log(values); + + var k = 0, code = [], i, j, length = 16; + while (length > 0 && !codeLengths[length - 1]) { + length--; + } + + code.push({ children: [], index: 0, }); + var p = code[0], q; + for (i = 0; i < length; i++) { + for (j = 0; j < codeLengths[i]; j++) { + p = code.pop(); + p.children[p.index] = values[k]; + + while (p.index > 0) { + p = code.pop(); + } + p.index++; + code.push(p); + while (code.length <= i) { + code.push(q = { children: [], index: 0, }); + p.children[p.index] = q.children; + p = q; + } + k++; + } + if (i + 1 < length) { + // p here points to last code + code.push(q = { children: [], index: 0, }); + p.children[p.index] = q.children; + p = q; + } + } + console.log(code[0].children); + console.log(k); + return code[0].children; + } + + function getBlockBufferOffset(component, row, col) { + return 64 * ((component.blocksPerLine + 1) * row + col); + } + + function decodeScan(data, offset, frame, components, resetInterval, + spectralStart, spectralEnd, successivePrev, successive) { + var mcusPerLine = frame.mcusPerLine; + var progressive = frame.progressive; + var startOffset = offset, bitsData = 0, bitsCount = 0; + + function readBit() { + if (bitsCount > 0) { + bitsCount--; + return (bitsData >> bitsCount) & 1; + } + bitsData = data[offset++]; + if (bitsData === 0xFF) { + var nextByte = data[offset++]; + if (nextByte) { + error('JPEG error: unexpected marker ' + + ((bitsData << 8) | nextByte).toString(16)); + } + // unstuff 0 + } + bitsCount = 7; + return bitsData >>> 7; + } + + function decodeHuffman(tree) { + var node = tree; + while (true) { + node = node[readBit()]; + if (typeof node === 'number') { + return node; + } + if (typeof node !== 'object') { + error('JPEG error: invalid huffman sequence'); + } + } + } + + function receive(length) { + var n = 0; + while (length > 0) { + n = (n << 1) | readBit(); + length--; + } + return n; + } + + function receiveAndExtend(length) { + if (length === 1) { + return readBit() === 1 ? 1 : -1; + } + var n = receive(length); + if (n >= 1 << (length - 1)) { + return n; + } + return n + (-1 << length) + 1; + } + + function decodeBaseline(component, offset) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : receiveAndExtend(t); + component.blockData[offset] = (component.pred += diff); + // console.log("component"); + // console.log(component); + + if(offset === 0){ + console.log("component at 0"); + console.log(component.blockData[offset]) + } + + var k = 1; + while (k < 64) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + component.blockData[offset + z] = receiveAndExtend(s); + k++; + } + } + + function decodeDCFirst(component, offset) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : (receiveAndExtend(t) << successive); + component.blockData[offset] = (component.pred += diff); + } + + function decodeDCSuccessive(component, offset) { + component.blockData[offset] |= readBit() << successive; + } + + var eobrun = 0; + function decodeACFirst(component, offset) { + if (eobrun > 0) { + eobrun--; + return; + } + var k = spectralStart, e = spectralEnd; + while (k <= e) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r) - 1; + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + component.blockData[offset + z] = + receiveAndExtend(s) * (1 << successive); + k++; + } + } + + var successiveACState = 0, successiveACNextValue; + function decodeACSuccessive(component, offset) { + var k = spectralStart; + var e = spectralEnd; + var r = 0; + var s; + var rs; + while (k <= e) { + var z = dctZigZag[k]; + switch (successiveACState) { + case 0: // initial state + rs = decodeHuffman(component.huffmanTableAC); + s = rs & 15; + r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r); + successiveACState = 4; + } else { + r = 16; + successiveACState = 1; + } + } else { + if (s !== 1) { + error('JPEG error: invalid ACn encoding'); + } + successiveACNextValue = receiveAndExtend(s); + successiveACState = r ? 2 : 3; + } + continue; + case 1: // skipping r zero items + case 2: + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } else { + r--; + if (r === 0) { + successiveACState = successiveACState === 2 ? 3 : 0; + } + } + break; + case 3: // set value for a zero item + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } else { + component.blockData[offset + z] = + successiveACNextValue << successive; + successiveACState = 0; + } + break; + case 4: // eob + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } + break; + } + k++; + } + if (successiveACState === 4) { + eobrun--; + if (eobrun === 0) { + successiveACState = 0; + } + } + } + + function decodeMcu(component, decode, mcu, row, col) { + var mcuRow = (mcu / mcusPerLine) | 0; + var mcuCol = mcu % mcusPerLine; + var blockRow = mcuRow * component.v + row; + var blockCol = mcuCol * component.h + col; + var offset = getBlockBufferOffset(component, blockRow, blockCol); + + // console.log("MCU Offset: " + offset); + decode(component, offset); + } + + function decodeBlock(component, decode, mcu) { + var blockRow = (mcu / component.blocksPerLine) | 0; + var blockCol = mcu % component.blocksPerLine; + var offset = getBlockBufferOffset(component, blockRow, blockCol); + decode(component, offset); + } + + var componentsLength = components.length; + var component, i, j, k, n; + var decodeFn; + if (progressive) { + if (spectralStart === 0) { + decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive; + console.log(successivePrev === 0 ? "decodeDCFirst" : "decodeDCSuccessive"); + } else { + decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive; + console.log(successivePrev === 0 ? "decodeACFirst" : "decodeACSuccessive"); + } + } else { + decodeFn = decodeBaseline; + } + + var mcu = 0, fileMarker; + var mcuExpected; + if (componentsLength === 1) { + mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn; + } else { + mcuExpected = mcusPerLine * frame.mcusPerColumn; + } + + console.log("mcuExpected = "+ mcuExpected); + + var h, v; + while (mcu < mcuExpected) { + // reset interval stuff + var mcuToRead = resetInterval ? + Math.min(mcuExpected - mcu, resetInterval) : mcuExpected; + for (i = 0; i < componentsLength; i++) { + components[i].pred = 0; + } + eobrun = 0; + + if (componentsLength === 1) { + component = components[0]; + + for (n = 0; n < mcuToRead; n++) { + decodeBlock(component, decodeFn, mcu); + mcu++; + } + } else { + for (n = 0; n < mcuToRead; n++) { + for (i = 0; i < componentsLength; i++) { + component = components[i]; + h = component.h; + v = component.v; + for (j = 0; j < v; j++) { + for (k = 0; k < h; k++) { + decodeMcu(component, decodeFn, mcu, j, k); + } + } + } + mcu++; + } + } + + // find marker + bitsCount = 0; + fileMarker = findNextFileMarker(data, offset); + // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // those to attempt to find a valid marker (fixes issue4090.pdf). + if (fileMarker && fileMarker.invalid) { + warn('decodeScan - unexpected MCU data, next marker is: ' + + fileMarker.invalid); + offset = fileMarker.offset; + } + var marker = fileMarker && fileMarker.marker; + if (!marker || marker <= 0xFF00) { + error('JPEG error: marker was not found'); + } + + if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx + offset += 2; + } else { + break; + } + } + + fileMarker = findNextFileMarker(data, offset); + // Some images include more Scan blocks than expected, skip past those and + // attempt to find the next valid marker (fixes issue8182.pdf). + if (fileMarker && fileMarker.invalid) { + warn('decodeScan - unexpected Scan data, next marker is: ' + + fileMarker.invalid); + offset = fileMarker.offset; + } + + return offset - startOffset; + } + + // A port of poppler's IDCT method which in turn is taken from: + // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + // 'Practical Fast 1-D DCT Algorithms with 11 Multiplications', + // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, + // 988-991. + function quantizeAndInverse(component, blockBufferOffset, p) { + var qt = component.quantizationTable, blockData = component.blockData; + var v0, v1, v2, v3, v4, v5, v6, v7; + var p0, p1, p2, p3, p4, p5, p6, p7; + var t; + + if (!qt) { + error('JPEG error: missing required Quantization Table.'); + } + + // inverse DCT on rows + for (var row = 0; row < 64; row += 8) { + // gather block data + p0 = blockData[blockBufferOffset + row]; + p1 = blockData[blockBufferOffset + row + 1]; + p2 = blockData[blockBufferOffset + row + 2]; + p3 = blockData[blockBufferOffset + row + 3]; + p4 = blockData[blockBufferOffset + row + 4]; + p5 = blockData[blockBufferOffset + row + 5]; + p6 = blockData[blockBufferOffset + row + 6]; + p7 = blockData[blockBufferOffset + row + 7]; + + // dequant p0 + p0 *= qt[row]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) { + t = (dctSqrt2 * p0 + 512) >> 10; + p[row] = t; + p[row + 1] = t; + p[row + 2] = t; + p[row + 3] = t; + p[row + 4] = t; + p[row + 5] = t; + p[row + 6] = t; + p[row + 7] = t; + continue; + } + // dequant p1 ... p7 + p1 *= qt[row + 1]; + p2 *= qt[row + 2]; + p3 *= qt[row + 3]; + p4 *= qt[row + 4]; + p5 *= qt[row + 5]; + p6 *= qt[row + 6]; + p7 *= qt[row + 7]; + + // stage 4 + v0 = (dctSqrt2 * p0 + 128) >> 8; + v1 = (dctSqrt2 * p4 + 128) >> 8; + v2 = p2; + v3 = p6; + v4 = (dctSqrt1d2 * (p1 - p7) + 128) >> 8; + v7 = (dctSqrt1d2 * (p1 + p7) + 128) >> 8; + v5 = p3 << 4; + v6 = p5 << 4; + + // stage 3 + v0 = (v0 + v1 + 1) >> 1; + v1 = v0 - v1; + t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[row] = v0 + v7; + p[row + 7] = v0 - v7; + p[row + 1] = v1 + v6; + p[row + 6] = v1 - v6; + p[row + 2] = v2 + v5; + p[row + 5] = v2 - v5; + p[row + 3] = v3 + v4; + p[row + 4] = v3 - v4; + } + + // inverse DCT on columns + for (var col = 0; col < 8; ++col) { + p0 = p[col]; + p1 = p[col + 8]; + p2 = p[col + 16]; + p3 = p[col + 24]; + p4 = p[col + 32]; + p5 = p[col + 40]; + p6 = p[col + 48]; + p7 = p[col + 56]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) { + t = (dctSqrt2 * p0 + 8192) >> 14; + // convert to 8 bit + t = (t < -2040) ? 0 : (t >= 2024) ? 255 : (t + 2056) >> 4; + blockData[blockBufferOffset + col] = t; + blockData[blockBufferOffset + col + 8] = t; + blockData[blockBufferOffset + col + 16] = t; + blockData[blockBufferOffset + col + 24] = t; + blockData[blockBufferOffset + col + 32] = t; + blockData[blockBufferOffset + col + 40] = t; + blockData[blockBufferOffset + col + 48] = t; + blockData[blockBufferOffset + col + 56] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p0 + 2048) >> 12; + v1 = (dctSqrt2 * p4 + 2048) >> 12; + v2 = p2; + v3 = p6; + v4 = (dctSqrt1d2 * (p1 - p7) + 2048) >> 12; + v7 = (dctSqrt1d2 * (p1 + p7) + 2048) >> 12; + v5 = p3; + v6 = p5; + + // stage 3 + // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when + // converting to UInt8 range later. + v0 = ((v0 + v1 + 1) >> 1) + 4112; + v1 = v0 - v1; + t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p0 = v0 + v7; + p7 = v0 - v7; + p1 = v1 + v6; + p6 = v1 - v6; + p2 = v2 + v5; + p5 = v2 - v5; + p3 = v3 + v4; + p4 = v3 - v4; + + // convert to 8-bit integers + p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? 255 : p0 >> 4; + p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? 255 : p1 >> 4; + p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? 255 : p2 >> 4; + p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? 255 : p3 >> 4; + p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? 255 : p4 >> 4; + p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? 255 : p5 >> 4; + p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? 255 : p6 >> 4; + p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? 255 : p7 >> 4; + + // store block data + blockData[blockBufferOffset + col] = p0; + blockData[blockBufferOffset + col + 8] = p1; + blockData[blockBufferOffset + col + 16] = p2; + blockData[blockBufferOffset + col + 24] = p3; + blockData[blockBufferOffset + col + 32] = p4; + blockData[blockBufferOffset + col + 40] = p5; + blockData[blockBufferOffset + col + 48] = p6; + blockData[blockBufferOffset + col + 56] = p7; + } + } + + function buildComponentData(frame, component) { + var blocksPerLine = component.blocksPerLine; + var blocksPerColumn = component.blocksPerColumn; + var computationBuffer = new Int16Array(64); + console.log("qt"); + console.log(component.quantizationTable); + for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) { + for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) { + var offset = getBlockBufferOffset(component, blockRow, blockCol); + quantizeAndInverse(component, offset, computationBuffer); + } + } + + console.log("component.blockData"); + console.log(component.blockData); + return component.blockData; + } + + function clamp0to255(a) { + return a <= 0 ? 0 : a >= 255 ? 255 : a; + } + + function findNextFileMarker(data, currentPos, startPos) { + function peekUint16(pos) { + return (data[pos] << 8) | data[pos + 1]; + } + + var maxPos = data.length - 1; + var newPos = startPos < currentPos ? startPos : currentPos; + + if (currentPos >= maxPos) { + return null; // Don't attempt to read non-existent data and just return. + } + var currentMarker = peekUint16(currentPos); + if (currentMarker >= 0xFFC0 && currentMarker <= 0xFFFE) { + return { + invalid: null, + marker: currentMarker, + offset: currentPos, + }; + } + var newMarker = peekUint16(newPos); + while (!(newMarker >= 0xFFC0 && newMarker <= 0xFFFE)) { + if (++newPos >= maxPos) { + return null; // Don't attempt to read non-existent data and just return. + } + newMarker = peekUint16(newPos); + } + return { + invalid: currentMarker.toString(16), + marker: newMarker, + offset: newPos, + }; + } + + JpegImage.prototype = { + parse: function parse(data) { + + function readUint16() { + var value = (data[offset] << 8) | data[offset + 1]; + offset += 2; + return value; + } + + function readDataBlock() { + var length = readUint16(); + var endOffset = offset + length - 2; + + var fileMarker = findNextFileMarker(data, endOffset, offset); + if (fileMarker && fileMarker.invalid) { + warn('readDataBlock - incorrect length, next marker is: ' + + fileMarker.invalid); + endOffset = fileMarker.offset; + } + + var array = data.subarray(offset, endOffset); + offset += array.length; + return array; + } + + function prepareComponents(frame) { + var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH); + var mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV); + for (var i = 0; i < frame.components.length; i++) { + component = frame.components[i]; + var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * + component.h / frame.maxH); + var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * + component.v / frame.maxV); + var blocksPerLineForMcu = mcusPerLine * component.h; + var blocksPerColumnForMcu = mcusPerColumn * component.v; + + var blocksBufferSize = 64 * blocksPerColumnForMcu * + (blocksPerLineForMcu + 1); + component.blockData = new Int16Array(blocksBufferSize); + component.blocksPerLine = blocksPerLine; + component.blocksPerColumn = blocksPerColumn; + } + frame.mcusPerLine = mcusPerLine; + frame.mcusPerColumn = mcusPerColumn; + } + + var offset = 0; + var jfif = null; + var adobe = null; + var frame, resetInterval; + var quantizationTables = []; + var huffmanTablesAC = [], huffmanTablesDC = []; + var fileMarker = readUint16(); + if (fileMarker !== 0xFFD8) { // SOI (Start of Image) + error('JPEG error: SOI not found'); + } + + fileMarker = readUint16(); + while (fileMarker !== 0xFFD9) { // EOI (End of image) + var i, j, l; + switch (fileMarker) { + case 0xFFE0: // APP0 (Application Specific) + case 0xFFE1: // APP1 + case 0xFFE2: // APP2 + case 0xFFE3: // APP3 + case 0xFFE4: // APP4 + case 0xFFE5: // APP5 + case 0xFFE6: // APP6 + case 0xFFE7: // APP7 + case 0xFFE8: // APP8 + case 0xFFE9: // APP9 + case 0xFFEA: // APP10 + case 0xFFEB: // APP11 + case 0xFFEC: // APP12 + case 0xFFED: // APP13 + case 0xFFEE: // APP14 + case 0xFFEF: // APP15 + case 0xFFFE: // COM (Comment) + var appData = readDataBlock(); + + if (fileMarker === 0xFFE0) { + if (appData[0] === 0x4A && appData[1] === 0x46 && + appData[2] === 0x49 && appData[3] === 0x46 && + appData[4] === 0) { // 'JFIF\x00' + jfif = { + version: { major: appData[5], minor: appData[6], }, + densityUnits: appData[7], + xDensity: (appData[8] << 8) | appData[9], + yDensity: (appData[10] << 8) | appData[11], + thumbWidth: appData[12], + thumbHeight: appData[13], + thumbData: appData.subarray(14, 14 + + 3 * appData[12] * appData[13]), + }; + } + } + // TODO APP1 - Exif + if (fileMarker === 0xFFEE) { + if (appData[0] === 0x41 && appData[1] === 0x64 && + appData[2] === 0x6F && appData[3] === 0x62 && + appData[4] === 0x65) { // 'Adobe' + adobe = { + version: (appData[5] << 8) | appData[6], + flags0: (appData[7] << 8) | appData[8], + flags1: (appData[9] << 8) | appData[10], + transformCode: appData[11], + }; + } + } + break; + + case 0xFFDB: // DQT (Define Quantization Tables) + var quantizationTablesLength = readUint16(); + var quantizationTablesEnd = quantizationTablesLength + offset - 2; + var z; + while (offset < quantizationTablesEnd) { + var quantizationTableSpec = data[offset++]; + var tableData = new Uint16Array(64); + if ((quantizationTableSpec >> 4) === 0) { // 8 bit values + for (j = 0; j < 64; j++) { + z = dctZigZag[j]; + tableData[z] = data[offset++]; + } + } else if ((quantizationTableSpec >> 4) === 1) { // 16 bit values + for (j = 0; j < 64; j++) { + z = dctZigZag[j]; + tableData[z] = readUint16(); + } + } else { + error('JPEG error: DQT - invalid table spec'); + } + quantizationTables[quantizationTableSpec & 15] = tableData; + } + break; + + case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT) + case 0xFFC1: // SOF1 (Start of Frame, Extended DCT) + case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT) + if (frame) { + error('JPEG error: Only single frame JPEGs supported'); + } + console.log("filemarker"); + console.log(fileMarker); + console.log(offset); + readUint16(); // skip data length + frame = {}; + frame.extended = (fileMarker === 0xFFC1); + frame.progressive = (fileMarker === 0xFFC2); + frame.precision = data[offset++]; + frame.scanLines = readUint16(); + frame.samplesPerLine = readUint16(); + frame.components = []; + frame.componentIds = {}; + var componentsCount = data[offset++], componentId; + var maxH = 0, maxV = 0; + for (i = 0; i < componentsCount; i++) { + componentId = data[offset]; + var h = data[offset + 1] >> 4; + var v = data[offset + 1] & 15; + if (maxH < h) { + maxH = h; + } + if (maxV < v) { + maxV = v; + } + var qId = data[offset + 2]; + l = frame.components.push({ + h, + v, + quantizationId: qId, + quantizationTable: null, // See comment below. + }); + frame.componentIds[componentId] = l - 1; + offset += 3; + } + + frame.maxH = maxH; + frame.maxV = maxV; + prepareComponents(frame); + break; + + case 0xFFC4: // DHT (Define Huffman Tables) + var huffmanLength = readUint16(); + for (i = 2; i < huffmanLength;) { + console.log("offset= " + offset); + var huffmanTableSpec = data[offset++]; + console.log("huffmanTableSpec= " + huffmanTableSpec); + + var codeLengths = new Uint8Array(16); + var codeLengthSum = 0; + for (j = 0; j < 16; j++, offset++) { + codeLengthSum += (codeLengths[j] = data[offset]); + } + console.log("codelengthsum = " + codeLengthSum); + console.log("offset = " + offset); + var huffmanValues = new Uint8Array(codeLengthSum); + for (j = 0; j < codeLengthSum; j++, offset++) { + huffmanValues[j] = data[offset]; + } + i += 17 + codeLengthSum; + + + console.log((huffmanTableSpec >> 4) === 0 ? "DC":"AC"); + ((huffmanTableSpec >> 4) === 0 + ? huffmanTablesDC + : huffmanTablesAC)[huffmanTableSpec & 15] = + buildHuffmanTable(codeLengths, huffmanValues); + } + break; + + case 0xFFDD: // DRI (Define Restart Interval) + readUint16(); // skip data length + resetInterval = readUint16(); + break; + + case 0xFFDA: // SOS (Start of Scan) + readUint16(); // scanLength + var selectorsCount = data[offset++]; + var components = [], component; + for (i = 0; i < selectorsCount; i++) { + var ci = data[offset++]; + console.log("ci= " + ci); + console.log("offset= " + offset); + + var componentIndex = frame.componentIds[ci]; + console.log("componentIndex= " + componentIndex); + component = frame.components[componentIndex]; + var tableSpec = data[offset++]; + component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4]; + component.huffmanTableAC = huffmanTablesAC[tableSpec & 15]; + components.push(component); + } + console.log("components= " + components); + + var spectralStart = data[offset++]; + var spectralEnd = data[offset++]; + var successiveApproximation = data[offset++]; + + console.log(frame.componentIds); + console.log("spectralStart= " + spectralStart); + console.log("spectralEnd= " + spectralEnd); + console.log("successiveApproximation= " + successiveApproximation); + // console.log("components before") + // console.log(components) + var processed = decodeScan(data, offset, + frame, components, resetInterval, + spectralStart, spectralEnd, + successiveApproximation >> 4, successiveApproximation & 15); + offset += processed; + console.log("components after"); + // console.log(frame); + for (var i = 0; i < 3; i++){ + for (var j = 0; j < 10; j++){ + console.log("component ["+ i +"] : value ["+j+"] ="+ frame.components[i].blockData[j]+"]"); + } + } + break; + + case 0xFFFF: // Fill bytes + if (data[offset] !== 0xFF) { // Avoid skipping a valid marker. + offset--; + } + break; + + default: + if (data[offset - 3] === 0xFF && + data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) { + // could be incorrect encoding -- last 0xFF byte of the previous + // block was eaten by the encoder + offset -= 3; + break; + } + + // TODO: Delete this after testing + fileMarker = 0xFFD9; + // error('JPEG error: unknown marker ' + fileMarker.toString(16)); + } + fileMarker = readUint16(); + } + + console.log("quantizationTables"); + console.log(quantizationTables); + + this.width = frame.samplesPerLine; + this.height = frame.scanLines; + this.jfif = jfif; + this.adobe = adobe; + this.components = []; + for (i = 0; i < frame.components.length; i++) { + component = frame.components[i]; + + // Prevent errors when DQT markers are placed after SOF{n} markers, + // by assigning the `quantizationTable` entry after the entire image + // has been parsed (fixes issue7406.pdf). + var quantizationTable = quantizationTables[component.quantizationId]; + if (quantizationTable) { + component.quantizationTable = quantizationTable; + } + + this.components.push({ + output: buildComponentData(frame, component), + scaleX: component.h / frame.maxH, + scaleY: component.v / frame.maxV, + blocksPerLine: component.blocksPerLine, + blocksPerColumn: component.blocksPerColumn, + }); + } + + console.log("components"); + console.log(this.components); + this.numComponents = this.components.length; + }, + + _getLinearizedBlockData: function getLinearizedBlockData(width, height) { + var scaleX = this.width / width, scaleY = this.height / height; + + var component, componentScaleX, componentScaleY, blocksPerScanline; + var x, y, i, j, k; + var index; + var offset = 0; + var output; + var numComponents = this.components.length; + var dataLength = width * height * numComponents; + var data = new Uint8Array(dataLength); + var xScaleBlockOffset = new Uint32Array(width); + var mask3LSB = 0xfffffff8; // used to clear the 3 LSBs + + for (i = 0; i < numComponents; i++) { + component = this.components[i]; + componentScaleX = component.scaleX * scaleX; + componentScaleY = component.scaleY * scaleY; + offset = i; + output = component.output; + blocksPerScanline = (component.blocksPerLine + 1) << 3; + // precalculate the xScaleBlockOffset + for (x = 0; x < width; x++) { + j = 0 | (x * componentScaleX); + xScaleBlockOffset[x] = ((j & mask3LSB) << 3) | (j & 7); + } + // linearize the blocks of the component + for (y = 0; y < height; y++) { + j = 0 | (y * componentScaleY); + index = blocksPerScanline * (j & mask3LSB) | ((j & 7) << 3); + for (x = 0; x < width; x++) { + data[offset] = output[index + xScaleBlockOffset[x]]; + offset += numComponents; + } + } + } + + // decodeTransform contains pairs of multiplier (-256..256) and additive + var transform = this.decodeTransform; + if (transform) { + for (i = 0; i < dataLength;) { + for (j = 0, k = 0; j < numComponents; j++, i++, k += 2) { + data[i] = ((data[i] * transform[k]) >> 8) + transform[k + 1]; + } + } + } + return data; + }, + + _isColorConversionNeeded: function isColorConversionNeeded() { + if (this.adobe && this.adobe.transformCode) { + // The adobe transform marker overrides any previous setting + return true; + } else if (this.numComponents === 3) { + if (!this.adobe && this.colorTransform === 0) { + // If the Adobe transform marker is not present and the image + // dictionary has a 'ColorTransform' entry, explicitly set to `0`, + // then the colours should *not* be transformed. + return false; + } + return true; + } + // `this.numComponents !== 3` + if (!this.adobe && this.colorTransform === 1) { + // If the Adobe transform marker is not present and the image + // dictionary has a 'ColorTransform' entry, explicitly set to `1`, + // then the colours should be transformed. + return true; + } + return false; + }, + + _convertYccToRgb: function convertYccToRgb(data) { + var Y, Cb, Cr; + for (var i = 0, length = data.length; i < length; i += 3) { + Y = data[i ]; + Cb = data[i + 1]; + Cr = data[i + 2]; + data[i ] = clamp0to255(Y - 179.456 + 1.402 * Cr); + data[i + 1] = clamp0to255(Y + 135.459 - 0.344 * Cb - 0.714 * Cr); + data[i + 2] = clamp0to255(Y - 226.816 + 1.772 * Cb); + } + return data; + }, + + _convertYcckToRgb: function convertYcckToRgb(data) { + var Y, Cb, Cr, k; + var offset = 0; + for (var i = 0, length = data.length; i < length; i += 4) { + Y = data[i]; + Cb = data[i + 1]; + Cr = data[i + 2]; + k = data[i + 3]; + + var r = -122.67195406894 + + Cb * (-6.60635669420364e-5 * Cb + 0.000437130475926232 * Cr - + 5.4080610064599e-5 * Y + 0.00048449797120281 * k - + 0.154362151871126) + + Cr * (-0.000957964378445773 * Cr + 0.000817076911346625 * Y - + 0.00477271405408747 * k + 1.53380253221734) + + Y * (0.000961250184130688 * Y - 0.00266257332283933 * k + + 0.48357088451265) + + k * (-0.000336197177618394 * k + 0.484791561490776); + + var g = 107.268039397724 + + Cb * (2.19927104525741e-5 * Cb - 0.000640992018297945 * Cr + + 0.000659397001245577 * Y + 0.000426105652938837 * k - + 0.176491792462875) + + Cr * (-0.000778269941513683 * Cr + 0.00130872261408275 * Y + + 0.000770482631801132 * k - 0.151051492775562) + + Y * (0.00126935368114843 * Y - 0.00265090189010898 * k + + 0.25802910206845) + + k * (-0.000318913117588328 * k - 0.213742400323665); + + var b = -20.810012546947 + + Cb * (-0.000570115196973677 * Cb - 2.63409051004589e-5 * Cr + + 0.0020741088115012 * Y - 0.00288260236853442 * k + + 0.814272968359295) + + Cr * (-1.53496057440975e-5 * Cr - 0.000132689043961446 * Y + + 0.000560833691242812 * k - 0.195152027534049) + + Y * (0.00174418132927582 * Y - 0.00255243321439347 * k + + 0.116935020465145) + + k * (-0.000343531996510555 * k + 0.24165260232407); + + data[offset++] = clamp0to255(r); + data[offset++] = clamp0to255(g); + data[offset++] = clamp0to255(b); + } + return data; + }, + + _convertYcckToCmyk: function convertYcckToCmyk(data) { + var Y, Cb, Cr; + for (var i = 0, length = data.length; i < length; i += 4) { + Y = data[i]; + Cb = data[i + 1]; + Cr = data[i + 2]; + data[i ] = clamp0to255(434.456 - Y - 1.402 * Cr); + data[i + 1] = clamp0to255(119.541 - Y + 0.344 * Cb + 0.714 * Cr); + data[i + 2] = clamp0to255(481.816 - Y - 1.772 * Cb); + // K in data[i + 3] is unchanged + } + return data; + }, + + _convertCmykToRgb: function convertCmykToRgb(data) { + var c, m, y, k; + var offset = 0; + var min = -255 * 255 * 255; + var scale = 1 / 255 / 255; + for (var i = 0, length = data.length; i < length; i += 4) { + c = data[i]; + m = data[i + 1]; + y = data[i + 2]; + k = data[i + 3]; + + var r = + c * (-4.387332384609988 * c + 54.48615194189176 * m + + 18.82290502165302 * y + 212.25662451639585 * k - + 72734.4411664936) + + m * (1.7149763477362134 * m - 5.6096736904047315 * y - + 17.873870861415444 * k - 1401.7366389350734) + + y * (-2.5217340131683033 * y - 21.248923337353073 * k + + 4465.541406466231) - + k * (21.86122147463605 * k + 48317.86113160301); + var g = + c * (8.841041422036149 * c + 60.118027045597366 * m + + 6.871425592049007 * y + 31.159100130055922 * k - + 20220.756542821975) + + m * (-15.310361306967817 * m + 17.575251261109482 * y + + 131.35250912493976 * k - 48691.05921601825) + + y * (4.444339102852739 * y + 9.8632861493405 * k - + 6341.191035517494) - + k * (20.737325471181034 * k + 47890.15695978492); + var b = + c * (0.8842522430003296 * c + 8.078677503112928 * m + + 30.89978309703729 * y - 0.23883238689178934 * k - + 3616.812083916688) + + m * (10.49593273432072 * m + 63.02378494754052 * y + + 50.606957656360734 * k - 28620.90484698408) + + y * (0.03296041114873217 * y + 115.60384449646641 * k - + 49363.43385999684) - + k * (22.33816807309886 * k + 45932.16563550634); + + data[offset++] = r >= 0 ? 255 : r <= min ? 0 : 255 + r * scale | 0; + data[offset++] = g >= 0 ? 255 : g <= min ? 0 : 255 + g * scale | 0; + data[offset++] = b >= 0 ? 255 : b <= min ? 0 : 255 + b * scale | 0; + } + return data; + }, + + getData: function getData(width, height, forceRGBoutput) { + if (this.numComponents > 4) { + error('JPEG error: Unsupported color mode'); + } + // type of data: Uint8Array(width * height * numComponents) + var data = this._getLinearizedBlockData(width, height); + + if (this.numComponents === 1 && forceRGBoutput) { + var dataLength = data.length; + var rgbData = new Uint8Array(dataLength * 3); + var offset = 0; + for (var i = 0; i < dataLength; i++) { + var grayColor = data[i]; + rgbData[offset++] = grayColor; + rgbData[offset++] = grayColor; + rgbData[offset++] = grayColor; + } + return rgbData; + } else if (this.numComponents === 3 && this._isColorConversionNeeded()) { + return this._convertYccToRgb(data); + } else if (this.numComponents === 4) { + if (this._isColorConversionNeeded()) { + if (forceRGBoutput) { + return this._convertYcckToRgb(data); + } + return this._convertYcckToCmyk(data); + } else if (forceRGBoutput) { + return this._convertCmykToRgb(data); + } + } + return data; + }, + }; + + return JpegImage; +})(); + +// export { +// JpegImage, +// }; diff --git a/tests/ImageSharp.Tests/xunit.runner.json b/tests/ImageSharp.Tests/xunit.runner.json index df1c3d50d..cbaa8f432 100644 --- a/tests/ImageSharp.Tests/xunit.runner.json +++ b/tests/ImageSharp.Tests/xunit.runner.json @@ -1,3 +1,4 @@ { - "methodDisplay": "method" + "methodDisplay": "method", + "diagnosticMessages": true } \ No newline at end of file