diff --git a/src/ImageSharp/Common/Helpers/Vector4Utils.cs b/src/ImageSharp/Common/Helpers/Vector4Utils.cs index 75bb00b6a..5c122217d 100644 --- a/src/ImageSharp/Common/Helpers/Vector4Utils.cs +++ b/src/ImageSharp/Common/Helpers/Vector4Utils.cs @@ -5,6 +5,7 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Primitives; namespace SixLabors.ImageSharp { @@ -70,5 +71,41 @@ namespace SixLabors.ImageSharp UnPremultiply(ref v); } } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Transform(ref Vector4 vector, ref ColorMatrix matrix) + { + float x = vector.X; + float y = vector.Y; + float z = vector.Z; + float w = vector.W; + + vector.X = (x * matrix.M11) + (y * matrix.M21) + (z * matrix.M31) + (w * matrix.M41) + matrix.M51; + vector.Y = (x * matrix.M12) + (y * matrix.M22) + (z * matrix.M32) + (w * matrix.M42) + matrix.M52; + vector.Z = (x * matrix.M13) + (y * matrix.M23) + (z * matrix.M33) + (w * matrix.M43) + matrix.M53; + vector.W = (x * matrix.M14) + (y * matrix.M24) + (z * matrix.M34) + (w * matrix.M44) + matrix.M54; + } + + /// + /// Bulk variant of + /// + /// The span of vectors + /// The transformation matrix. + [MethodImpl(InliningOptions.ShortMethod)] + public static void Transform(Span vectors, ref ColorMatrix matrix) + { + ref Vector4 baseRef = ref MemoryMarshal.GetReference(vectors); + + for (int i = 0; i < vectors.Length; i++) + { + ref Vector4 v = ref Unsafe.Add(ref baseRef, i); + Transform(ref v, ref matrix); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Common/Tuples/Vector4Pair.cs b/src/ImageSharp/Common/Tuples/Vector4Pair.cs index cae283d62..2e5ca3137 100644 --- a/src/ImageSharp/Common/Tuples/Vector4Pair.cs +++ b/src/ImageSharp/Common/Tuples/Vector4Pair.cs @@ -37,12 +37,11 @@ namespace SixLabors.ImageSharp.Tuples this.B += other.B; } - /// - /// Downscale method, specific to Jpeg color conversion. Works only if Vector{float}.Count == 4! - /// TODO: Move it somewhere else. + /// . + /// Downscale method, specific to Jpeg color conversion. Works only if Vector{float}.Count == 4! /// TODO: Move it somewhere else. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void RoundAndDownscalePreAvx2() + internal void RoundAndDownscalePreAvx2(float downscaleFactor) { ref Vector a = ref Unsafe.As>(ref this.A); a = a.FastRound(); @@ -50,8 +49,8 @@ namespace SixLabors.ImageSharp.Tuples ref Vector b = ref Unsafe.As>(ref this.B); b = b.FastRound(); - // Downscale by 1/255 - var scale = new Vector4(1 / 255f); + // Downscale by 1/factor + var scale = new Vector4(1 / downscaleFactor); this.A *= scale; this.B *= scale; } @@ -61,14 +60,14 @@ namespace SixLabors.ImageSharp.Tuples /// TODO: Move it somewhere else. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void RoundAndDownscaleAvx2() + internal void RoundAndDownscaleAvx2(float downscaleFactor) { ref Vector self = ref Unsafe.As>(ref this); Vector v = self; v = v.FastRound(); - // Downscale by 1/255 - v *= new Vector(1 / 255f); + // Downscale by 1/factor + v *= new Vector(1 / downscaleFactor); self = v; } diff --git a/src/ImageSharp/Formats/Bmp/BmpCompression.cs b/src/ImageSharp/Formats/Bmp/BmpCompression.cs index ef063f010..5f14d2243 100644 --- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs +++ b/src/ImageSharp/Formats/Bmp/BmpCompression.cs @@ -25,7 +25,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// If the first byte is zero, the record has different meanings, depending /// on the second byte. If the second byte is zero, it is the end of the row, /// if it is one, it is the end of the image. - /// Not supported at the moment. /// RLE8 = 1, @@ -42,7 +41,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Each image row has a multiple of four elements. If the /// row has less elements, zeros will be added at the right side. - /// Not supported at the moment. /// BitFields = 3, diff --git a/src/ImageSharp/Formats/Bmp/BmpConstants.cs b/src/ImageSharp/Formats/Bmp/BmpConstants.cs index 99799b619..5cbed4af2 100644 --- a/src/ImageSharp/Formats/Bmp/BmpConstants.cs +++ b/src/ImageSharp/Formats/Bmp/BmpConstants.cs @@ -19,5 +19,41 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The list of file extensions that equate to a bmp. /// public static readonly IEnumerable FileExtensions = new[] { "bm", "bmp", "dip" }; + + /// + /// Valid magic bytes markers identifying a Bitmap file. + /// + internal static class TypeMarkers + { + /// + /// Single-image BMP file that may have been created under Windows or OS/2. + /// + public const int Bitmap = 0x4D42; + + /// + /// OS/2 Bitmap Array. + /// + public const int BitmapArray = 0x4142; + + /// + /// OS/2 Color Icon. + /// + public const int ColorIcon = 0x4943; + + /// + /// OS/2 Color Pointer. + /// + public const int ColorPointer = 0x5043; + + /// + /// OS/2 Icon. + /// + public const int Icon = 0x4349; + + /// + /// OS/2 Pointer. + /// + public const int Pointer = 0x5450; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index 3d079cf61..82af2a671 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -15,8 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// JPG /// PNG /// RLE4 - /// RLE8 - /// BitFields /// /// Formats will be supported in a later releases. We advise always /// to use only 24 Bit Windows bitmaps. diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 68528edcd..8ca698b87 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Buffers.Binary; using System.IO; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; @@ -14,7 +16,7 @@ using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Bmp { /// - /// Performs the bmp decoding operation. + /// Performs the bitmap decoding operation. /// /// /// A useful decoding source example can be found at @@ -22,19 +24,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp internal sealed class BmpDecoderCore { /// - /// The mask for the red part of the color for 16 bit rgb bitmaps. + /// The default mask for the red part of the color for 16 bit rgb bitmaps. /// - private const int Rgb16RMask = 0x7C00; + private const int DefaultRgb16RMask = 0x7C00; /// - /// The mask for the green part of the color for 16 bit rgb bitmaps. + /// The default mask for the green part of the color for 16 bit rgb bitmaps. /// - private const int Rgb16GMask = 0x3E0; + private const int DefaultRgb16GMask = 0x3E0; /// - /// The mask for the blue part of the color for 16 bit rgb bitmaps. + /// The default mask for the blue part of the color for 16 bit rgb bitmaps. /// - private const int Rgb16BMask = 0x1F; + private const int DefaultRgb16BMask = 0x1F; /// /// RLE8 flag value that indicates following byte has special meaning. @@ -62,10 +64,15 @@ namespace SixLabors.ImageSharp.Formats.Bmp private Stream stream; /// - /// The metadata + /// The metadata. /// private ImageMetaData metaData; + /// + /// The bmp specific metadata. + /// + private BmpMetaData bmpMetaData; + /// /// The file header containing general information. /// TODO: Why is this not used? We advance the stream but do not use the values parsed. @@ -85,7 +92,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Initializes a new instance of the class. /// /// The configuration. - /// The options + /// The options. public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options) { this.configuration = configuration; @@ -119,7 +126,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp case BmpCompression.RGB: if (this.infoHeader.BitsPerPixel == 32) { - this.ReadRgb32(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + if (this.bmpMetaData.InfoHeaderType == BmpInfoHeaderType.WinVersion3) + { + this.ReadRgb32Slow(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + } + else + { + this.ReadRgb32Fast(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); + } } else if (this.infoHeader.BitsPerPixel == 24) { @@ -143,9 +157,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp break; case BmpCompression.RLE8: - this.ReadRle8(pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted); + case BmpCompression.RLE4: + this.ReadRle(this.infoHeader.Compression, pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted); + + break; + + case BmpCompression.BitFields: + this.ReadBitFields(pixels, inverted); break; + default: throw new NotSupportedException("Does not support this kind of bitmap files."); } @@ -199,30 +220,66 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Performs final shifting from a 5bit value to an 8bit one. + /// Decodes a bitmap containing BITFIELDS Compression type. For each color channel, there will be bitmask + /// which will be used to determine which bits belong to that channel. /// - /// The masked and shifted value - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2)); + /// The pixel format. + /// The output pixel buffer containing the decoded image. + /// Whether the bitmap is inverted. + private void ReadBitFields(Buffer2D pixels, bool inverted) + where TPixel : struct, IPixel + { + if (this.infoHeader.BitsPerPixel == 16) + { + this.ReadRgb16( + pixels, + this.infoHeader.Width, + this.infoHeader.Height, + inverted, + this.infoHeader.RedMask, + this.infoHeader.GreenMask, + this.infoHeader.BlueMask); + } + else + { + this.ReadRgb32BitFields( + pixels, + this.infoHeader.Width, + this.infoHeader.Height, + inverted, + this.infoHeader.RedMask, + this.infoHeader.GreenMask, + this.infoHeader.BlueMask, + this.infoHeader.AlphaMask); + } + } /// - /// Looks up color values and builds the image from de-compressed RLE8 data. + /// Looks up color values and builds the image from de-compressed RLE8 or RLE4 data. /// Compressed RLE8 stream is uncompressed by + /// Compressed RLE4 stream is uncompressed by /// /// The pixel format. + /// The compression type. Either RLE4 or RLE8. /// The to assign the palette to. /// The containing the colors. /// The width of the bitmap. /// The height of the bitmap. /// Whether the bitmap is inverted. - private void ReadRle8(Buffer2D pixels, byte[] colors, int width, int height, bool inverted) + private void ReadRle(BmpCompression compression, Buffer2D pixels, byte[] colors, int width, int height, bool inverted) where TPixel : struct, IPixel { TPixel color = default; using (Buffer2D buffer = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) { - this.UncompressRle8(width, buffer.GetSpan()); + if (compression == BmpCompression.RLE8) + { + this.UncompressRle8(width, buffer.GetSpan()); + } + else + { + this.UncompressRle4(width, buffer.GetSpan()); + } for (int y = 0; y < height; y++) { @@ -240,12 +297,122 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Produce uncompressed bitmap data from RLE8 stream + /// Produce uncompressed bitmap data from a RLE4 stream. + /// + /// + /// RLE4 is a 2-byte run-length encoding. + ///
If first byte is 0, the second byte may have special meaning. + ///
Otherwise, the first byte is the length of the run and second byte contains two color indexes. + ///
+ /// The width of the bitmap. + /// Buffer for uncompressed data. + private void UncompressRle4(int w, Span buffer) + { +#if NETCOREAPP2_1 + Span cmd = stackalloc byte[2]; +#else + byte[] cmd = new byte[2]; +#endif + int count = 0; + + while (count < buffer.Length) + { + if (this.stream.Read(cmd, 0, cmd.Length) != 2) + { + throw new Exception("Failed to read 2 bytes from the stream"); + } + + if (cmd[0] == RleCommand) + { + switch (cmd[1]) + { + case RleEndOfBitmap: + return; + + case RleEndOfLine: + int extra = count % w; + if (extra > 0) + { + count += w - extra; + } + + break; + + case RleDelta: + int dx = this.stream.ReadByte(); + int dy = this.stream.ReadByte(); + count += (w * dy) + dx; + + break; + + default: + // If the second byte > 2, we are in 'absolute mode'. + // The second byte contains the number of color indexes that follow. + int max = cmd[1]; + int bytesToRead = (max + 1) / 2; + + byte[] run = new byte[bytesToRead]; + + this.stream.Read(run, 0, run.Length); + + int idx = 0; + for (int i = 0; i < max; i++) + { + byte twoPixels = run[idx]; + if (i % 2 == 0) + { + byte leftPixel = (byte)((twoPixels >> 4) & 0xF); + buffer[count++] = leftPixel; + } + else + { + byte rightPixel = (byte)(twoPixels & 0xF); + buffer[count++] = rightPixel; + idx++; + } + } + + // Absolute mode data is aligned to two-byte word-boundary + int padding = bytesToRead & 1; + + this.stream.Skip(padding); + + break; + } + } + else + { + int max = cmd[0]; + + // The second byte contains two color indexes, one in its high-order 4 bits and one in its low-order 4 bits. + byte twoPixels = cmd[1]; + byte rightPixel = (byte)(twoPixels & 0xF); + byte leftPixel = (byte)((twoPixels >> 4) & 0xF); + + for (int idx = 0; idx < max; idx++) + { + if (idx % 2 == 0) + { + buffer[count] = leftPixel; + } + else + { + buffer[count] = rightPixel; + } + + count++; + } + } + } + } + + /// + /// Produce uncompressed bitmap data from a RLE8 stream. /// /// - /// RLE8 is a 2-byte run-length encoding - ///
If first byte is 0, the second byte may have special meaning - ///
Otherwise, first byte is the length of the run and second byte is the color for the run + /// RLE8 is a 2-byte run-length encoding. + ///
If first byte is 0, the second byte may have special meaning. + ///
Otherwise, the first byte is the length of the run and second byte is the color for the run. ///
/// The width of the bitmap. /// Buffer for uncompressed data. @@ -382,20 +549,32 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Reads the 16 bit color palette from the stream + /// Reads the 16 bit color palette from the stream. /// /// The pixel format. /// The to assign the palette to. /// The width of the bitmap. /// The height of the bitmap. /// Whether the bitmap is inverted. - private void ReadRgb16(Buffer2D pixels, int width, int height, bool inverted) + /// The bitmask for the red channel. + /// The bitmask for the green channel. + /// The bitmask for the blue channel. + private void ReadRgb16(Buffer2D pixels, int width, int height, bool inverted, int redMask = DefaultRgb16RMask, int greenMask = DefaultRgb16GMask, int blueMask = DefaultRgb16BMask) where TPixel : struct, IPixel { int padding = CalculatePadding(width, 2); int stride = (width * 2) + padding; TPixel color = default; + int rightShiftRedMask = CalculateRightShift((uint)redMask); + int rightShiftGreenMask = CalculateRightShift((uint)greenMask); + int rightShiftBlueMask = CalculateRightShift((uint)blueMask); + + // Each color channel contains either 5 or 6 Bits values. + int redMaskBits = CountBits((uint)redMask); + int greenMaskBits = CountBits((uint)greenMask); + int blueMaskBits = CountBits((uint)blueMask); + using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(stride)) { for (int y = 0; y < height; y++) @@ -409,10 +588,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp { short temp = BitConverter.ToInt16(buffer.Array, offset); - var rgb = new Rgb24( - GetBytesFrom5BitValue((temp & Rgb16RMask) >> 10), - GetBytesFrom5BitValue((temp & Rgb16GMask) >> 5), - GetBytesFrom5BitValue(temp & Rgb16BMask)); + // Rescale values, so the values range from 0 to 255. + int r = (redMaskBits == 5) ? GetBytesFrom5BitValue((temp & redMask) >> rightShiftRedMask) : GetBytesFrom6BitValue((temp & redMask) >> rightShiftRedMask); + int g = (greenMaskBits == 5) ? GetBytesFrom5BitValue((temp & greenMask) >> rightShiftGreenMask) : GetBytesFrom6BitValue((temp & greenMask) >> rightShiftGreenMask); + int b = (blueMaskBits == 5) ? GetBytesFrom5BitValue((temp & blueMask) >> rightShiftBlueMask) : GetBytesFrom6BitValue((temp & blueMask) >> rightShiftBlueMask); + var rgb = new Rgb24((byte)r, (byte)g, (byte)b); color.FromRgb24(rgb); pixelRow[x] = color; @@ -423,7 +603,23 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Reads the 24 bit color palette from the stream + /// Performs final shifting from a 5bit value to an 8bit one. + /// + /// The masked and shifted value. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2)); + + /// + /// Performs final shifting from a 6bit value to an 8bit one. + /// + /// The masked and shifted value. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetBytesFrom6BitValue(int value) => (byte)((value << 2) | (value >> 4)); + + /// + /// Reads the 24 bit color palette from the stream. /// /// The pixel format. /// The to assign the palette to. @@ -452,14 +648,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Reads the 32 bit color palette from the stream + /// Reads the 32 bit color palette from the stream. /// /// The pixel format. /// The to assign the palette to. /// The width of the bitmap. /// The height of the bitmap. /// Whether the bitmap is inverted. - private void ReadRgb32(Buffer2D pixels, int width, int height, bool inverted) + private void ReadRgb32Fast(Buffer2D pixels, int width, int height, bool inverted) where TPixel : struct, IPixel { int padding = CalculatePadding(width, 4); @@ -480,6 +676,228 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } + /// + /// Reads the 32 bit color palette from the stream, checking the alpha component of each pixel. + /// This is a special case only used for 32bpp WinBMPv3 files, which could be in either BGR0 or BGRA format. + /// + /// The pixel format. + /// The to assign the palette to. + /// The width of the bitmap. + /// The height of the bitmap. + /// Whether the bitmap is inverted. + private void ReadRgb32Slow(Buffer2D pixels, int width, int height, bool inverted) + where TPixel : struct, IPixel + { + int padding = CalculatePadding(width, 4); + + using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, padding)) + using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width)) + { + Span bgraRowSpan = bgraRow.GetSpan(); + long currentPosition = this.stream.Position; + bool hasAlpha = false; + + // Loop though the rows checking each pixel. We start by assuming it's + // an BGR0 image. If we hit a non-zero alpha value, then we know it's + // actually a BGRA image, and change tactics accordingly. + for (int y = 0; y < height; y++) + { + this.stream.Read(row); + + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + bgraRowSpan, + width); + + // Check each pixel in the row to see if it has an alpha value. + for (int x = 0; x < width; x++) + { + Bgra32 bgra = bgraRowSpan[x]; + if (bgra.A > 0) + { + hasAlpha = true; + break; + } + } + + if (hasAlpha) + { + break; + } + } + + // Reset our stream for a second pass. + this.stream.Position = currentPosition; + + // Process the pixels in bulk taking the raw alpha component value. + if (hasAlpha) + { + for (int y = 0; y < height; y++) + { + this.stream.Read(row); + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + pixelSpan, + width); + } + + return; + } + + // Slow path. We need to set each alpha component value to fully opaque. + for (int y = 0; y < height; y++) + { + this.stream.Read(row); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row.GetSpan(), + bgraRowSpan, + width); + + int newY = Invert(y, height, inverted); + Span pixelSpan = pixels.GetRowSpan(newY); + + for (int x = 0; x < width; x++) + { + Bgra32 bgra = bgraRowSpan[x]; + bgra.A = byte.MaxValue; + ref TPixel pixel = ref pixelSpan[x]; + pixel.FromBgra32(bgra); + } + } + } + } + + /// + /// Decode an 32 Bit Bitmap containing a bitmask for each color channel. + /// + /// The pixel format. + /// The output pixel buffer containing the decoded image. + /// The width of the image. + /// The height of the image. + /// Whether the bitmap is inverted. + /// The bitmask for the red channel. + /// The bitmask for the green channel. + /// The bitmask for the blue channel. + /// The bitmask for the alpha channel. + private void ReadRgb32BitFields(Buffer2D pixels, int width, int height, bool inverted, int redMask, int greenMask, int blueMask, int alphaMask) + where TPixel : struct, IPixel + { + TPixel color = default; + int padding = CalculatePadding(width, 4); + int stride = (width * 4) + padding; + + int rightShiftRedMask = CalculateRightShift((uint)redMask); + int rightShiftGreenMask = CalculateRightShift((uint)greenMask); + int rightShiftBlueMask = CalculateRightShift((uint)blueMask); + int rightShiftAlphaMask = CalculateRightShift((uint)alphaMask); + + int bitsRedMask = CountBits((uint)redMask); + int bitsGreenMask = CountBits((uint)greenMask); + int bitsBlueMask = CountBits((uint)blueMask); + int bitsAlphaMask = CountBits((uint)alphaMask); + float invMaxValueRed = 1.0f / (0xFFFFFFFF >> (32 - bitsRedMask)); + float invMaxValueGreen = 1.0f / (0xFFFFFFFF >> (32 - bitsGreenMask)); + float invMaxValueBlue = 1.0f / (0xFFFFFFFF >> (32 - bitsBlueMask)); + uint maxValueAlpha = 0xFFFFFFFF >> (32 - bitsAlphaMask); + float invMaxValueAlpha = 1.0f / maxValueAlpha; + + bool unusualBitMask = false; + if (bitsRedMask > 8 || bitsGreenMask > 8 || bitsBlueMask > 8 || invMaxValueAlpha > 8) + { + unusualBitMask = true; + } + + using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(stride)) + { + for (int y = 0; y < height; y++) + { + this.stream.Read(buffer.Array, 0, stride); + int newY = Invert(y, height, inverted); + Span pixelRow = pixels.GetRowSpan(newY); + + int offset = 0; + for (int x = 0; x < width; x++) + { + uint temp = BitConverter.ToUInt32(buffer.Array, offset); + + if (unusualBitMask) + { + uint r = (uint)(temp & redMask) >> rightShiftRedMask; + uint g = (uint)(temp & greenMask) >> rightShiftGreenMask; + uint b = (uint)(temp & blueMask) >> rightShiftBlueMask; + float alpha = alphaMask != 0 ? invMaxValueAlpha * ((uint)(temp & alphaMask) >> rightShiftAlphaMask) : 1.0f; + var vector4 = new Vector4( + r * invMaxValueRed, + g * invMaxValueGreen, + b * invMaxValueBlue, + alpha); + color.FromVector4(vector4); + } + else + { + byte r = (byte)((temp & redMask) >> rightShiftRedMask); + byte g = (byte)((temp & greenMask) >> rightShiftGreenMask); + byte b = (byte)((temp & blueMask) >> rightShiftBlueMask); + byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : (byte)255; + color.FromRgba32(new Rgba32(r, g, b, a)); + } + + pixelRow[x] = color; + offset += 4; + } + } + } + } + + /// + /// Calculates the necessary right shifts for a given color bitmask (the 0 bits to the right). + /// + /// The color bit mask. + /// Number of bits to shift right. + private static int CalculateRightShift(uint n) + { + int count = 0; + while (n > 0) + { + if ((1 & n) == 0) + { + count++; + } + else + { + break; + } + + n >>= 1; + } + + return count; + } + + /// + /// Counts none zero bits. + /// + /// A color mask. + /// The none zero bits. + private static int CountBits(uint n) + { + int count = 0; + while (n != 0) + { + count++; + n &= n - 1; + } + + return count; + } + /// /// Reads the from the stream. /// @@ -508,20 +926,54 @@ namespace SixLabors.ImageSharp.Formats.Bmp // read the rest of the header this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize); + BmpInfoHeaderType inofHeaderType = BmpInfoHeaderType.WinVersion2; if (headerSize == BmpInfoHeader.CoreSize) { // 12 bytes + inofHeaderType = BmpInfoHeaderType.WinVersion2; this.infoHeader = BmpInfoHeader.ParseCore(buffer); } else if (headerSize == BmpInfoHeader.Os22ShortSize) { // 16 bytes + inofHeaderType = BmpInfoHeaderType.Os2Version2Short; this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer); } - else if (headerSize >= BmpInfoHeader.Size) + else if (headerSize == BmpInfoHeader.SizeV3) + { + // == 40 bytes + inofHeaderType = BmpInfoHeaderType.WinVersion3; + this.infoHeader = BmpInfoHeader.ParseV3(buffer); + + // if the info header is BMP version 3 and the compression type is BITFIELDS, + // color masks for each color channel follow the info header. + if (this.infoHeader.Compression == BmpCompression.BitFields) + { + byte[] bitfieldsBuffer = new byte[12]; + this.stream.Read(bitfieldsBuffer, 0, 12); + Span data = bitfieldsBuffer.AsSpan(); + this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)); + this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); + this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); + } + } + else if (headerSize == BmpInfoHeader.AdobeV3Size) + { + // == 52 bytes + inofHeaderType = BmpInfoHeaderType.AdobeVersion3; + this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: false); + } + else if (headerSize == BmpInfoHeader.AdobeV3WithAlphaSize) + { + // == 56 bytes + inofHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha; + this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: true); + } + else if (headerSize >= BmpInfoHeader.SizeV4) { - // >= 40 bytes - this.infoHeader = BmpInfoHeader.Parse(buffer); + // >= 108 bytes + inofHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5; + this.infoHeader = BmpInfoHeader.ParseV4(buffer); } else { @@ -548,13 +1000,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.metaData = meta; short bitsPerPixel = this.infoHeader.BitsPerPixel; - BmpMetaData bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance); + this.bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance); + this.bmpMetaData.InfoHeaderType = inofHeaderType; // We can only encode at these bit rates so far. if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24) || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32)) { - bmpMetaData.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; + this.bmpMetaData.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; } // skip the remaining header because we can't read those parts diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index a67c581eb..27a38bc0d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -23,6 +23,26 @@ namespace SixLabors.ImageSharp.Formats.Bmp ///
private int padding; + /// + /// The mask for the alpha channel of the color for a 32 bit rgba bitmaps. + /// + private const int Rgba32AlphaMask = 0xFF << 24; + + /// + /// The mask for the red part of the color for a 32 bit rgba bitmaps. + /// + private const int Rgba32RedMask = 0xFF << 16; + + /// + /// The mask for the green part of the color for a 32 bit rgba bitmaps. + /// + private const int Rgba32GreenMask = 0xFF << 8; + + /// + /// The mask for the blue part of the color for a 32 bit rgba bitmaps. + /// + private const int Rgba32BlueMask = 0xFF; + private readonly MemoryAllocator memoryAllocator; private Configuration configuration; @@ -92,8 +112,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } + int infoHeaderSize = BmpInfoHeader.SizeV4; var infoHeader = new BmpInfoHeader( - headerSize: BmpInfoHeader.Size, + headerSize: infoHeaderSize, height: image.Height, width: image.Width, bitsPerPixel: bpp, @@ -102,26 +123,37 @@ namespace SixLabors.ImageSharp.Formats.Bmp clrUsed: 0, clrImportant: 0, xPelsPerMeter: hResolution, - yPelsPerMeter: vResolution); + yPelsPerMeter: vResolution) + { + RedMask = Rgba32RedMask, + GreenMask = Rgba32GreenMask, + BlueMask = Rgba32BlueMask, + Compression = BmpCompression.BitFields + }; + + if (this.bitsPerPixel == BmpBitsPerPixel.Pixel32) + { + infoHeader.AlphaMask = Rgba32AlphaMask; + } var fileHeader = new BmpFileHeader( - type: 19778, // BM - fileSize: 54 + infoHeader.ImageSize, + type: BmpConstants.TypeMarkers.Bitmap, + fileSize: BmpFileHeader.Size + infoHeaderSize + infoHeader.ImageSize, reserved: 0, - offset: 54); + offset: BmpFileHeader.Size + infoHeaderSize); #if NETCOREAPP2_1 - Span buffer = stackalloc byte[40]; + Span buffer = stackalloc byte[infoHeaderSize]; #else - byte[] buffer = new byte[40]; + byte[] buffer = new byte[infoHeaderSize]; #endif fileHeader.WriteTo(buffer); stream.Write(buffer, 0, BmpFileHeader.Size); - infoHeader.WriteTo(buffer); + infoHeader.WriteV4Header(buffer); - stream.Write(buffer, 0, 40); + stream.Write(buffer, 0, infoHeaderSize); this.WriteImage(stream, image.Frames.RootFrame); diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 5177bc325..316df4acc 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -16,11 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct BmpInfoHeader { - /// - /// Defines the size of the BITMAPINFOHEADER data structure in the bitmap file. - /// - public const int Size = 40; - /// /// Defines the size of the BITMAPCOREHEADER data structure in the bitmap file. /// @@ -31,10 +26,30 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public const int Os22ShortSize = 16; + /// + /// Defines the size of the BITMAPINFOHEADER (BMP Version 3) data structure in the bitmap file. + /// + public const int SizeV3 = 40; + + /// + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. + /// + public const int AdobeV3Size = 52; + + /// + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks (including the alpha channel) are part of the info header instead of following it. + /// + public const int AdobeV3WithAlphaSize = 56; + + /// + /// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file. + /// + public const int SizeV4 = 108; + /// /// Defines the size of the biggest supported header data structure in the bitmap file. /// - public const int MaxHeaderSize = Size; + public const int MaxHeaderSize = SizeV4; /// /// Defines the size of the field. @@ -52,7 +67,24 @@ namespace SixLabors.ImageSharp.Formats.Bmp int xPelsPerMeter = 0, int yPelsPerMeter = 0, int clrUsed = 0, - int clrImportant = 0) + int clrImportant = 0, + int redMask = 0, + int greenMask = 0, + int blueMask = 0, + int alphaMask = 0, + int csType = 0, + int redX = 0, + int redY = 0, + int redZ = 0, + int greenX = 0, + int greenY = 0, + int greenZ = 0, + int blueX = 0, + int blueY = 0, + int blueZ = 0, + int gammeRed = 0, + int gammeGreen = 0, + int gammeBlue = 0) { this.HeaderSize = headerSize; this.Width = width; @@ -65,6 +97,23 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.YPelsPerMeter = yPelsPerMeter; this.ClrUsed = clrUsed; this.ClrImportant = clrImportant; + this.RedMask = redMask; + this.GreenMask = greenMask; + this.BlueMask = blueMask; + this.AlphaMask = alphaMask; + this.CsType = csType; + this.RedX = redX; + this.RedY = redY; + this.RedZ = redZ; + this.GreenX = greenX; + this.GreenY = greenY; + this.GreenZ = greenZ; + this.BlueX = blueX; + this.BlueY = blueY; + this.BlueZ = blueZ; + this.GammaRed = gammeRed; + this.GammaGreen = gammeGreen; + this.GammaBlue = gammeBlue; } /// @@ -130,23 +179,92 @@ namespace SixLabors.ImageSharp.Formats.Bmp public int ClrImportant { get; set; } /// - /// Parses the full BITMAPINFOHEADER header (40 bytes). + /// Gets or sets red color mask. This is used with the BITFIELDS decoding. /// - /// The data to parse. - /// Parsed header - /// - public static BmpInfoHeader Parse(ReadOnlySpan data) - { - if (data.Length != Size) - { - throw new ArgumentException(nameof(data), $"Must be {Size} bytes. Was {data.Length} bytes."); - } + public int RedMask { get; set; } - return MemoryMarshal.Cast(data)[0]; - } + /// + /// Gets or sets green color mask. This is used with the BITFIELDS decoding. + /// + public int GreenMask { get; set; } + + /// + /// Gets or sets blue color mask. This is used with the BITFIELDS decoding. + /// + public int BlueMask { get; set; } + + /// + /// Gets or sets alpha color mask. This is not used yet. + /// + public int AlphaMask { get; set; } + + /// + /// Gets or sets the Color space type. Not used yet. + /// + public int CsType { get; set; } + + /// + /// Gets or sets the X coordinate of red endpoint. Not used yet. + /// + public int RedX { get; set; } + + /// + /// Gets or sets the Y coordinate of red endpoint. Not used yet. + /// + public int RedY { get; set; } + + /// + /// Gets or sets the Z coordinate of red endpoint. Not used yet. + /// + public int RedZ { get; set; } + + /// + /// Gets or sets the X coordinate of green endpoint. Not used yet. + /// + public int GreenX { get; set; } + + /// + /// Gets or sets the Y coordinate of green endpoint. Not used yet. + /// + public int GreenY { get; set; } + + /// + /// Gets or sets the Z coordinate of green endpoint. Not used yet. + /// + public int GreenZ { get; set; } + + /// + /// Gets or sets the X coordinate of blue endpoint. Not used yet. + /// + public int BlueX { get; set; } /// - /// Parses the BITMAPCOREHEADER consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). + /// Gets or sets the Y coordinate of blue endpoint. Not used yet. + /// + public int BlueY { get; set; } + + /// + /// Gets or sets the Z coordinate of blue endpoint. Not used yet. + /// + public int BlueZ { get; set; } + + /// + /// Gets or sets the Gamma red coordinate scale value. Not used yet. + /// + public int GammaRed { get; set; } + + /// + /// Gets or sets the Gamma green coordinate scale value. Not used yet. + /// + public int GammaGreen { get; set; } + + /// + /// Gets or sets the Gamma blue coordinate scale value. Not used yet. + /// + public int GammaBlue { get; set; } + + /// + /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). /// /// The data to parse. /// Parsed header @@ -163,7 +281,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height - /// are 4 bytes instead of 2. + /// are 4 bytes instead of 2, resulting in 16 bytes total. /// /// The data to parse. /// Parsed header @@ -178,7 +296,97 @@ namespace SixLabors.ImageSharp.Formats.Bmp bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2))); } - public unsafe void WriteTo(Span buffer) + /// + /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV3(ReadOnlySpan data) + { + return new BmpInfoHeader( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4))); + } + + /// + /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. + /// 52 bytes without the alpha mask, 56 bytes with the alpha mask. + /// + /// The data to parse. + /// Indicates, if the alpha bitmask is present. + /// The parsed header. + /// + public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) + { + return new BmpInfoHeader( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)), + redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)), + greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), + blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), + alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); + } + + /// + /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV4(ReadOnlySpan data) + { + if (data.Length != SizeV4) + { + throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes."); + } + + return MemoryMarshal.Cast(data)[0]; + } + + /// + /// Writes a bitmap version 3 (Microsoft Windows NT) header to a buffer (40 bytes). + /// + /// The buffer to write to. + public void WriteV3Header(Span buffer) + { + buffer.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), SizeV3); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant); + } + + /// + /// Writes a complete Bitmap V4 header to a buffer. + /// + /// The buffer to write to. + public unsafe void WriteV4Header(Span buffer) { ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs new file mode 100644 index 000000000..a4ce0115f --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs @@ -0,0 +1,48 @@ +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Enum value for the different bitmap info header types. The enum value is the number of bytes for the specific bitmap header. + /// + public enum BmpInfoHeaderType + { + /// + /// Bitmap Core or BMP Version 2 header (Microsoft Windows 2.x). + /// + WinVersion2 = 12, + + /// + /// Short variant of the OS/2 Version 2 bitmap header. + /// + Os2Version2Short = 16, + + /// + /// BMP Version 3 header (Microsoft Windows 3.x or Microsoft Windows NT). + /// + WinVersion3 = 40, + + /// + /// Adobe variant of the BMP Version 3 header. + /// + AdobeVersion3 = 52, + + /// + /// Adobe variant of the BMP Version 3 header with an alpha mask. + /// + AdobeVersion3WithAlpha = 56, + + /// + /// BMP Version 2.x header (IBM OS/2 2.x). + /// + Os2Version2 = 64, + + /// + /// BMP Version 4 header (Microsoft Windows 95). + /// + WinVersion4 = 108, + + /// + /// BMP Version 5 header (Windows NT 5.0, 98 or later). + /// + WinVersion5 = 124, + } +} diff --git a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs index 8b33e30fa..2d8685617 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs @@ -19,7 +19,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private BmpMetaData(BmpMetaData other) => this.BitsPerPixel = other.BitsPerPixel; + private BmpMetaData(BmpMetaData other) + { + this.BitsPerPixel = other.BitsPerPixel; + this.InfoHeaderType = other.InfoHeaderType; + } + + /// + /// Gets or sets the bitmap info header type. + /// + public BmpInfoHeaderType InfoHeaderType { get; set; } /// /// Gets or sets the number of bits per pixel. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs index 09ed6408d..1f47de594 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Numerics; using System.Runtime.CompilerServices; @@ -9,10 +10,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { internal partial struct Block8x8F { - private static readonly Vector4 CMin4 = new Vector4(0F); - private static readonly Vector4 CMax4 = new Vector4(255F); - private static readonly Vector4 COff4 = new Vector4(128F); - /// /// Transpose the block into the destination block. /// @@ -94,10 +91,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } /// - /// Level shift by +128, clip to [0, 255] + /// Level shift by +maximum/2, clip to [0, maximum] /// - public void NormalizeColorsInplace() + public void NormalizeColorsInplace(float maximum) { + Vector4 CMin4 = new Vector4(0F); + Vector4 CMax4 = new Vector4(maximum); + Vector4 COff4 = new Vector4((float)Math.Ceiling(maximum/2)); + this.V0L = Vector4.Clamp(this.V0L + COff4, CMin4, CMax4); this.V0R = Vector4.Clamp(this.V0R + COff4, CMin4, CMax4); this.V1L = Vector4.Clamp(this.V1L + COff4, CMin4, CMax4); @@ -120,10 +121,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// AVX2-only variant for executing and in one step. /// [MethodImpl(InliningOptions.ShortMethod)] - public void NormalizeColorsAndRoundInplaceAvx2() + public void NormalizeColorsAndRoundInplaceAvx2(float maximum) { - Vector off = new Vector(128f); - Vector max = new Vector(255F); + Vector off = new Vector((float)Math.Ceiling(maximum/2)); + Vector max = new Vector(maximum); ref Vector row0 = ref Unsafe.As>(ref this.V0L); row0 = NormalizeAndRound(row0, off, max); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt index f93ee6522..ec4e06e42 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt @@ -11,6 +11,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Numerics; using System.Runtime.CompilerServices; @@ -22,10 +23,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { internal partial struct Block8x8F { - private static readonly Vector4 CMin4 = new Vector4(0F); - private static readonly Vector4 CMax4 = new Vector4(255F); - private static readonly Vector4 COff4 = new Vector4(128F); - /// /// Transpose the block into the destination block. /// @@ -59,10 +56,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } /// - /// Level shift by +128, clip to [0, 255] + /// Level shift by +maximum/2, clip to [0, maximum] /// - public void NormalizeColorsInplace() + public void NormalizeColorsInplace(float maximum) { + Vector4 CMin4 = new Vector4(0F); + Vector4 CMax4 = new Vector4(maximum); + Vector4 COff4 = new Vector4((float)Math.Ceiling(maximum/2)); + <# PushIndent(" "); @@ -83,10 +84,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// AVX2-only variant for executing and in one step. /// [MethodImpl(InliningOptions.ShortMethod)] - public void NormalizeColorsAndRoundInplaceAvx2() + public void NormalizeColorsAndRoundInplaceAvx2(float maximum) { - Vector off = new Vector(128f); - Vector max = new Vector(255F); + Vector off = new Vector((float)Math.Ceiling(maximum/2)); + Vector max = new Vector(maximum); <# for (int i = 0; i < 8; i++) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 81393342d..c9c886f05 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -467,17 +467,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } /// - /// Level shift by +128, clip to [0..255], and round all the values in the block. + /// Level shift by +maximum/2, clip to [0..maximum], and round all the values in the block. /// - public void NormalizeColorsAndRoundInplace() + public void NormalizeColorsAndRoundInplace(float maximum) { if (SimdUtils.IsAvx2CompatibleArchitecture) { - this.NormalizeColorsAndRoundInplaceAvx2(); + this.NormalizeColorsAndRoundInplaceAvx2(maximum); } else { - this.NormalizeColorsInplace(); + this.NormalizeColorsInplace(maximum); this.RoundInplace(); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmyk.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmyk.cs index 7a14d072e..d4dc31fe0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmyk.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmyk.cs @@ -10,8 +10,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromCmyk : JpegColorConverter { - public FromCmyk() - : base(JpegColorSpace.Cmyk) + public FromCmyk(int precision) + : base(JpegColorSpace.Cmyk, precision) { } @@ -25,14 +25,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1F); - var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); + var scale = new Vector4( + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1F); for (int i = 0; i < result.Length; i++) { float c = cVals[i]; float m = mVals[i]; float y = yVals[i]; - float k = kVals[i] / 255F; + float k = kVals[i] / this.MaximumValue; v.X = c * k; v.Y = m * k; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs index 7424145c3..4a5dfa632 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs @@ -12,14 +12,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromGrayscale : JpegColorConverter { - public FromGrayscale() - : base(JpegColorSpace.Grayscale) + public FromGrayscale(int precision) + : base(JpegColorSpace.Grayscale, precision) { } public override void ConvertToRgba(in ComponentValues values, Span result) { - var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); + var scale = new Vector4( + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1F); ref float sBase = ref MemoryMarshal.GetReference(values.Component0); ref Vector4 dBase = ref MemoryMarshal.GetReference(result); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgb.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgb.cs index 7cd97c414..516dfb39f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgb.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgb.cs @@ -10,8 +10,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromRgb : JpegColorConverter { - public FromRgb() - : base(JpegColorSpace.RGB) + public FromRgb(int precision) + : base(JpegColorSpace.RGB, precision) { } @@ -24,7 +24,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1); - var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); + var scale = new Vector4( + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1F); for (int i = 0; i < result.Length; i++) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs index cb71889bc..124aac122 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs @@ -10,17 +10,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromYCbCrBasic : JpegColorConverter { - public FromYCbCrBasic() - : base(JpegColorSpace.YCbCr) + public FromYCbCrBasic(int precision) + : base(JpegColorSpace.YCbCr, precision) { } public override void ConvertToRgba(in ComponentValues values, Span result) { - ConvertCore(values, result); + ConvertCore(values, result, this.MaximumValue, this.HalfValue); } - internal static void ConvertCore(in ComponentValues values, Span result) + internal static void ConvertCore(in ComponentValues values, Span result, float maxValue, float halfValue) { // TODO: We can optimize a lot here with Vector and SRCS.Unsafe()! ReadOnlySpan yVals = values.Component0; @@ -29,13 +29,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1); - var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); + var scale = new Vector4(1 / maxValue, 1 / maxValue, 1 / maxValue, 1F); for (int i = 0; i < result.Length; i++) { float y = yVals[i]; - float cb = cbVals[i] - 128F; - float cr = crVals[i] - 128F; + float cb = cbVals[i] - halfValue; + float cr = crVals[i] - halfValue; v.X = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero); v.Y = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs index 23aa1acbe..10ef02a93 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs @@ -14,8 +14,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromYCbCrSimd : JpegColorConverter { - public FromYCbCrSimd() - : base(JpegColorSpace.YCbCr) + public FromYCbCrSimd(int precision) + : base(JpegColorSpace.YCbCr, precision) { } @@ -25,16 +25,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters int simdCount = result.Length - remainder; if (simdCount > 0) { - ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount)); + ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount), this.MaximumValue, this.HalfValue); } - FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder)); + FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder), this.MaximumValue, this.HalfValue); } /// /// SIMD convert using buffers of sizes divisible by 8. /// - internal static void ConvertCore(in ComponentValues values, Span result) + internal static void ConvertCore(in ComponentValues values, Span result, float maxValue, float halfValue) { DebugGuard.IsTrue(result.Length % 8 == 0, nameof(result), "result.Length should be divisible by 8!"); @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters ref Vector4Octet resultBase = ref Unsafe.As(ref MemoryMarshal.GetReference(result)); - var chromaOffset = new Vector4(-128f); + var chromaOffset = new Vector4(-halfValue); // Walking 8 elements at one step: int n = result.Length / 8; @@ -58,11 +58,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters // y = yVals[i]; Vector4Pair y = Unsafe.Add(ref yBase, i); - // cb = cbVals[i] - 128F; + // cb = cbVals[i] - halfValue); Vector4Pair cb = Unsafe.Add(ref cbBase, i); cb.AddInplace(chromaOffset); - // cr = crVals[i] - 128F; + // cr = crVals[i] - halfValue; Vector4Pair cr = Unsafe.Add(ref crBase, i); cr.AddInplace(chromaOffset); @@ -90,15 +90,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters if (Vector.Count == 4) { // TODO: Find a way to properly run & test this path on AVX2 PC-s! (Have I already mentioned that Vector is terrible?) - r.RoundAndDownscalePreAvx2(); - g.RoundAndDownscalePreAvx2(); - b.RoundAndDownscalePreAvx2(); + r.RoundAndDownscalePreAvx2(maxValue); + g.RoundAndDownscalePreAvx2(maxValue); + b.RoundAndDownscalePreAvx2(maxValue); } else if (SimdUtils.IsAvx2CompatibleArchitecture) { - r.RoundAndDownscaleAvx2(); - g.RoundAndDownscaleAvx2(); - b.RoundAndDownscaleAvx2(); + r.RoundAndDownscaleAvx2(maxValue); + g.RoundAndDownscaleAvx2(maxValue); + b.RoundAndDownscaleAvx2(maxValue); } else { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs index f0a70a6f3..9953f78c1 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs @@ -15,8 +15,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromYCbCrSimdAvx2 : JpegColorConverter { - public FromYCbCrSimdAvx2() - : base(JpegColorSpace.YCbCr) + public FromYCbCrSimdAvx2(int precision) + : base(JpegColorSpace.YCbCr, precision) { } @@ -28,16 +28,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters int simdCount = result.Length - remainder; if (simdCount > 0) { - ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount)); + ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount), this.MaximumValue, this.HalfValue); } - FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder)); + FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder), this.MaximumValue, this.HalfValue); } /// /// SIMD convert using buffers of sizes divisible by 8. /// - internal static void ConvertCore(in ComponentValues values, Span result) + internal static void ConvertCore(in ComponentValues values, Span result, float maxValue, float halfValue) { // This implementation is actually AVX specific. // An AVX register is capable of storing 8 float-s. @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters ref Vector4Octet resultBase = ref Unsafe.As(ref MemoryMarshal.GetReference(result)); - var chromaOffset = new Vector(-128f); + var chromaOffset = new Vector(-halfValue); // Walking 8 elements at one step: int n = result.Length / 8; @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters ref Vector ggRefAsVector = ref Unsafe.As>(ref gg); ref Vector bbRefAsVector = ref Unsafe.As>(ref bb); - var scale = new Vector(1 / 255f); + var scale = new Vector(1 / maxValue); for (int i = 0; i < n; i++) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccK.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccK.cs index 6f940f62f..94be11e23 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccK.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccK.cs @@ -10,8 +10,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { internal class FromYccK : JpegColorConverter { - public FromYccK() - : base(JpegColorSpace.Ycck) + public FromYccK(int precision) + : base(JpegColorSpace.Ycck, precision) { } @@ -25,18 +25,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1F); - var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); + var scale = new Vector4( + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1 / this.MaximumValue, + 1F); for (int i = 0; i < result.Length; i++) { float y = yVals[i]; - float cb = cbVals[i] - 128F; - float cr = crVals[i] - 128F; - float k = kVals[i] / 255F; + float cb = cbVals[i] - this.HalfValue; + float cr = crVals[i] - this.HalfValue; + float k = kVals[i] / this.MaximumValue; - v.X = (255F - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; - v.Y = (255F - MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * k; - v.Z = (255F - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; + v.X = (this.MaximumValue - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; + v.Y = (this.MaximumValue - MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * k; + v.Z = (this.MaximumValue - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; v.W = 1F; v *= scale; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs index a44ebf89d..c2e390c59 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs @@ -3,12 +3,10 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tuples; -using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { @@ -22,15 +20,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// private static readonly JpegColorConverter[] Converters = { - GetYCbCrConverter(), new FromYccK(), new FromCmyk(), new FromGrayscale(), new FromRgb() + // 8-bit converters + GetYCbCrConverter(8), + new FromYccK(8), + new FromCmyk(8), + new FromGrayscale(8), + new FromRgb(8), + + // 12-bit converters + GetYCbCrConverter(12), + new FromYccK(12), + new FromCmyk(12), + new FromGrayscale(12), + new FromRgb(12), }; /// /// Initializes a new instance of the class. /// - protected JpegColorConverter(JpegColorSpace colorSpace) + protected JpegColorConverter(JpegColorSpace colorSpace, int precision) { this.ColorSpace = colorSpace; + this.Precision = precision; + this.MaximumValue = (float)Math.Pow(2, precision) - 1; + this.HalfValue = (float)Math.Ceiling(this.MaximumValue / 2); } /// @@ -38,12 +51,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// public JpegColorSpace ColorSpace { get; } + /// + /// Gets the Precision of this converter in bits. + /// + public int Precision { get; } + + /// + /// Gets the maximum value of a sample + /// + private float MaximumValue { get; } + + /// + /// Gets the half of the maximum value of a sample + /// + private float HalfValue { get; } + /// /// Returns the corresponding to the given /// - public static JpegColorConverter GetConverter(JpegColorSpace colorSpace) + public static JpegColorConverter GetConverter(JpegColorSpace colorSpace, float precision) { - JpegColorConverter converter = Converters.FirstOrDefault(c => c.ColorSpace == colorSpace); + JpegColorConverter converter = Array.Find(Converters, c => c.ColorSpace == colorSpace + && c.Precision == precision); if (converter is null) { @@ -63,8 +92,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// /// Returns the for the YCbCr colorspace that matches the current CPU architecture. /// - private static JpegColorConverter GetYCbCrConverter() => - FromYCbCrSimdAvx2.IsAvailable ? (JpegColorConverter)new FromYCbCrSimdAvx2() : new FromYCbCrSimd(); + private static JpegColorConverter GetYCbCrConverter(int precision) => + FromYCbCrSimdAvx2.IsAvailable ? (JpegColorConverter)new FromYCbCrSimdAvx2(precision) : new FromYCbCrSimd(precision); /// /// A stack-only struct to reference the input buffers using -s. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 9e134746b..9e11981b1 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -50,8 +50,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The huffman values public HuffmanTable(MemoryAllocator memoryAllocator, ReadOnlySpan codeLengths, ReadOnlySpan values) { - const int Length = 257; - using (IMemoryOwner huffcode = memoryAllocator.Allocate(Length)) + // We do some bounds checks in the code here to protect against AccessViolationExceptions + const int HuffCodeLength = 257; + const int MaxSizeLength = HuffCodeLength - 1; + using (IMemoryOwner huffcode = memoryAllocator.Allocate(HuffCodeLength)) { ref short huffcodeRef = ref MemoryMarshal.GetReference(huffcode.GetSpan()); ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengths); @@ -63,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (short i = 1; i < 17; i++) { byte length = Unsafe.Add(ref codeLengthsRef, i); - for (short j = 0; j < length; j++) + for (short j = 0; j < length && x < MaxSizeLength; j++) { Unsafe.Add(ref sizesRef, x++) = i; } @@ -84,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder Unsafe.Add(ref valOffsetRef, k) = (int)(si - code); if (Unsafe.Add(ref sizesRef, si) == k) { - while (Unsafe.Add(ref sizesRef, si) == k) + while (Unsafe.Add(ref sizesRef, si) == k && si < HuffCodeLength) { Unsafe.Add(ref huffcodeRef, si++) = (short)code++; } @@ -100,19 +102,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // Generate non-spec lookup tables to speed up decoding. const int FastBits = ScanDecoder.FastBits; - ref byte fastRef = ref this.Lookahead[0]; - Unsafe.InitBlockUnaligned(ref fastRef, 0xFF, 1 << FastBits); // Flag for non-accelerated + ref byte lookaheadRef = ref this.Lookahead[0]; + const uint MaxFastLength = 1 << FastBits; + Unsafe.InitBlockUnaligned(ref lookaheadRef, 0xFF, MaxFastLength); // Flag for non-accelerated for (int i = 0; i < si; i++) { int size = Unsafe.Add(ref sizesRef, i); if (size <= FastBits) { - int c = Unsafe.Add(ref huffcodeRef, i) << (FastBits - size); - int m = 1 << (FastBits - size); - for (int l = 0; l < m; l++) + int huffCode = Unsafe.Add(ref huffcodeRef, i) << (FastBits - size); + int max = 1 << (FastBits - size); + for (int left = 0; left < max; left++) { - Unsafe.Add(ref fastRef, c + l) = (byte)i; + Unsafe.Add(ref lookaheadRef, huffCode + left) = (byte)i; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs index 1454bb5b1..ace8d7215 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs @@ -29,10 +29,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// JpegColorSpace ColorSpace { get; } + /// + /// Gets the number of bits used for precision. + /// + int Precision { get; } + /// /// Gets the components. /// - IEnumerable Components { get; } + IJpegComponent[] Components { get; } /// /// Gets the quantization tables, in zigzag order. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs index da4b2847b..fe39f4188 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.Primitives; @@ -38,6 +39,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private Size subSamplingDivisors; + /// + /// Defines the maximum value derived from the bitdepth + /// + private int maximumValue; + /// /// Initializes a new instance of the struct. /// @@ -48,6 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int qtIndex = component.QuantizationTableIndex; this.DequantiazationTable = ZigZag.CreateDequantizationTable(ref decoder.QuantizationTables[qtIndex]); this.subSamplingDivisors = component.SubSamplingDivisors; + this.maximumValue = (int)Math.Pow(2, decoder.Precision) - 1; this.SourceBlock = default; this.WorkspaceBlock1 = default; @@ -58,14 +65,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Processes 'sourceBlock' producing Jpeg color channel values from spectral values: /// - Dequantize /// - Applying IDCT - /// - Level shift by +128, clip to [0, 255] + /// - Level shift by +maximumValue/2, clip to [0, maximumValue] /// - Copy the resulting color values into 'destArea' scaling up the block by amount defined in /// /// The source block. /// The destination buffer area. + /// The maximum value derived from the bitdepth. public void ProcessBlockColorsInto( ref Block8x8 sourceBlock, - in BufferArea destArea) + in BufferArea destArea, + float maximumValue) { ref Block8x8F b = ref this.SourceBlock; b.LoadFrom(ref sourceBlock); @@ -78,7 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // To conform better to libjpeg we actually NEED TO loose precision here. // This is because they store blocks as Int16 between all the operations. // To be "more accurate", we need to emulate this by rounding! - this.WorkspaceBlock1.NormalizeColorsAndRoundInplace(); + this.WorkspaceBlock1.NormalizeColorsAndRoundInplace(maximumValue); this.WorkspaceBlock1.CopyTo(destArea, this.subSamplingDivisors.Width, this.subSamplingDivisors.Height); } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index 94ec600dd..e7f3e4fda 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -78,6 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder public void CopyBlocksToColorBuffer() { var blockPp = new JpegBlockPostProcessor(this.ImagePostProcessor.RawJpeg, this.Component); + float maximumValue = (float)Math.Pow(2, this.ImagePostProcessor.RawJpeg.Precision) - 1; for (int y = 0; y < this.BlockRowsPerStep; y++) { @@ -105,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.blockAreaSize.Width, this.blockAreaSize.Height); - blockPp.ProcessBlockColorsInto(ref block, destArea); + blockPp.ProcessBlockColorsInto(ref block, destArea, maximumValue); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs index 7ce86b4c9..438749abf 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Linq; using System.Numerics; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -57,14 +56,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { this.configuration = configuration; this.RawJpeg = rawJpeg; - IJpegComponent c0 = rawJpeg.Components.First(); + IJpegComponent c0 = rawJpeg.Components[0]; this.NumberOfPostProcessorSteps = c0.SizeInBlocks.Height / BlockRowsPerStep; this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep); MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - this.ComponentProcessors = rawJpeg.Components.Select(c => new JpegComponentPostProcessor(memoryAllocator, this, c)).ToArray(); + + this.ComponentProcessors = new JpegComponentPostProcessor[rawJpeg.Components.Length]; + for (int i = 0; i < rawJpeg.Components.Length; i++) + { + this.ComponentProcessors[i] = new JpegComponentPostProcessor(memoryAllocator, this, rawJpeg.Components[i]); + } + this.rgbaBuffer = memoryAllocator.Allocate(rawJpeg.ImageSizeInPixels.Width); - this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace); + this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace, rawJpeg.Precision); } /// @@ -152,7 +157,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { int maxY = Math.Min(destination.Height, this.PixelRowCounter + PixelRowsPerStep); - Buffer2D[] buffers = this.ComponentProcessors.Select(cp => cp.ColorBuffer).ToArray(); + var buffers = new Buffer2D[this.ComponentProcessors.Length]; + for (int i = 0; i < this.ComponentProcessors.Length; i++) + { + buffers[i] = this.ComponentProcessors[i].ColorBuffer; + } for (int yy = this.PixelRowCounter; yy < maxY; yy++) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index faa7c3bc9..06c844d58 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -3,7 +3,6 @@ using System; using System.Buffers.Binary; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -33,7 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The only supported precision /// - public const int SupportedPrecision = 8; + private readonly int[] supportedPrecisions = { 8, 12 }; /// /// The global configuration @@ -137,7 +136,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Gets the color depth, in number of bits per pixel. /// - public int BitsPerPixel => this.ComponentCount * SupportedPrecision; + public int BitsPerPixel => this.ComponentCount * this.Frame.Precision; /// /// Gets the input stream. @@ -160,13 +159,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public JpegColorSpace ColorSpace { get; private set; } + /// + public int Precision { get; private set; } + /// /// Gets the components. /// public JpegComponent[] Components => this.Frame.Components; /// - IEnumerable IRawJpegData.Components => this.Components; + IJpegComponent[] IRawJpegData.Components => this.Components; /// public Block8x8F[] QuantizationTables { get; private set; } @@ -720,12 +722,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InputStream.Read(this.temp, 0, remaining); - // We only support 8-bit precision. - if (this.temp[0] != SupportedPrecision) + // We only support 8-bit and 12-bit precision. + if (!this.supportedPrecisions.Contains(this.temp[0])) { - throw new ImageFormatException("Only 8-Bit precision supported."); + throw new ImageFormatException("Only 8-Bit and 12-Bit precision supported."); } + this.Precision = this.temp[0]; + this.Frame = new JpegFrame { Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, @@ -855,6 +859,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void ProcessStartOfScanMarker() { + if (this.Frame is null) + { + throw new ImageFormatException("No readable SOFn (Start Of Frame) marker found."); + } + int selectorsCount = this.InputStream.ReadByte(); for (int i = 0; i < selectorsCount; i++) { diff --git a/src/ImageSharp/Primitives/ColorMatrix.cs b/src/ImageSharp/Primitives/ColorMatrix.cs new file mode 100644 index 000000000..af2e9465a --- /dev/null +++ b/src/ImageSharp/Primitives/ColorMatrix.cs @@ -0,0 +1,459 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +#pragma warning disable SA1117 // Parameters should be on same line or separate lines +using System; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Primitives +{ + /// + /// A structure encapsulating a 5x4 matrix used for transforming the color and alpha components of an image. + /// + [StructLayout(LayoutKind.Sequential)] + public struct ColorMatrix : IEquatable + { + /// + /// Value at row 1, column 1 of the matrix. + /// + public float M11; + + /// + /// Value at row 1, column 2 of the matrix. + /// + public float M12; + + /// + /// Value at row 1, column 3 of the matrix. + /// + public float M13; + + /// + /// Value at row 1, column 4 of the matrix. + /// + public float M14; + + /// + /// Value at row 2, column 1 of the matrix. + /// + public float M21; + + /// + /// Value at row 2, column 2 of the matrix. + /// + public float M22; + + /// + /// Value at row 2, column 3 of the matrix. + /// + public float M23; + + /// + /// Value at row 2, column 4 of the matrix. + /// + public float M24; + + /// + /// Value at row 3, column 1 of the matrix. + /// + public float M31; + + /// + /// Value at row 3, column 2 of the matrix. + /// + public float M32; + + /// + /// Value at row 3, column 3 of the matrix. + /// + public float M33; + + /// + /// Value at row 3, column 4 of the matrix. + /// + public float M34; + + /// + /// Value at row 4, column 1 of the matrix. + /// + public float M41; + + /// + /// Value at row 4, column 2 of the matrix. + /// + public float M42; + + /// + /// Value at row 4, column 3 of the matrix. + /// + public float M43; + + /// + /// Value at row 4, column 4 of the matrix. + /// + public float M44; + + /// + /// Value at row 5, column 1 of the matrix. + /// + public float M51; + + /// + /// Value at row 5, column 2 of the matrix. + /// + public float M52; + + /// + /// Value at row 5, column 3 of the matrix. + /// + public float M53; + + /// + /// Value at row 5, column 4 of the matrix. + /// + public float M54; + + /// + /// Initializes a new instance of the struct. + /// + /// The value at row 1, column 1 of the matrix. + /// The value at row 1, column 2 of the matrix. + /// The value at row 1, column 3 of the matrix. + /// The value at row 1, column 4 of the matrix. + /// The value at row 2, column 1 of the matrix. + /// The value at row 2, column 2 of the matrix. + /// The value at row 2, column 3 of the matrix. + /// The value at row 2, column 4 of the matrix. + /// The value at row 3, column 1 of the matrix. + /// The value at row 3, column 2 of the matrix. + /// The value at row 3, column 3 of the matrix. + /// The value at row 3, column 4 of the matrix. + /// The value at row 4, column 1 of the matrix. + /// The value at row 4, column 2 of the matrix. + /// The value at row 4, column 3 of the matrix. + /// The value at row 4, column 4 of the matrix. + /// The value at row 5, column 1 of the matrix. + /// The value at row 5, column 2 of the matrix. + /// The value at row 5, column 3 of the matrix. + /// The value at row 5, column 4 of the matrix. + public ColorMatrix(float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44, + float m51, float m52, float m53, float m54) + { + this.M11 = m11; + this.M12 = m12; + this.M13 = m13; + this.M14 = m14; + + this.M21 = m21; + this.M22 = m22; + this.M23 = m23; + this.M24 = m24; + + this.M31 = m31; + this.M32 = m32; + this.M33 = m33; + this.M34 = m34; + + this.M41 = m41; + this.M42 = m42; + this.M43 = m43; + this.M44 = m44; + + this.M51 = m51; + this.M52 = m52; + this.M53 = m53; + this.M54 = m54; + } + + /// + /// Gets the multiplicative identity matrix. + /// + public static ColorMatrix Identity { get; } = + new ColorMatrix(1F, 0F, 0F, 0F, + 0F, 1F, 0F, 0F, + 0F, 0F, 1F, 0F, + 0F, 0F, 0F, 1F, + 0F, 0F, 0F, 0F); + + /// + /// Gets a value indicating whether the matrix is the identity matrix. + /// + public bool IsIdentity + { + get + { + // Check diagonal element first for early out. + return this.M11 == 1F && this.M22 == 1F && this.M33 == 1F && this.M44 == 1F + && this.M12 == 0F && this.M13 == 0F && this.M14 == 0F + && this.M21 == 0F && this.M23 == 0F && this.M24 == 0F + && this.M31 == 0F && this.M32 == 0F && this.M34 == 0F + && this.M41 == 0F && this.M42 == 0F && this.M43 == 0F + && this.M51 == 0F && this.M52 == 0F && this.M53 == 0F && this.M54 == 0F; + } + } + + /// + /// Adds two matrices together. + /// + /// The first source matrix. + /// The second source matrix. + /// The resulting matrix. + public static ColorMatrix operator +(ColorMatrix value1, ColorMatrix value2) + { + ColorMatrix m; + + m.M11 = value1.M11 + value2.M11; + m.M12 = value1.M12 + value2.M12; + m.M13 = value1.M13 + value2.M13; + m.M14 = value1.M14 + value2.M14; + m.M21 = value1.M21 + value2.M21; + m.M22 = value1.M22 + value2.M22; + m.M23 = value1.M23 + value2.M23; + m.M24 = value1.M24 + value2.M24; + m.M31 = value1.M31 + value2.M31; + m.M32 = value1.M32 + value2.M32; + m.M33 = value1.M33 + value2.M33; + m.M34 = value1.M34 + value2.M34; + m.M41 = value1.M41 + value2.M41; + m.M42 = value1.M42 + value2.M42; + m.M43 = value1.M43 + value2.M43; + m.M44 = value1.M44 + value2.M44; + m.M51 = value1.M51 + value2.M51; + m.M52 = value1.M52 + value2.M52; + m.M53 = value1.M53 + value2.M53; + m.M54 = value1.M54 + value2.M54; + + return m; + } + + /// + /// Subtracts the second matrix from the first. + /// + /// The first source matrix. + /// The second source matrix. + /// The result of the subtraction. + public static ColorMatrix operator -(ColorMatrix value1, ColorMatrix value2) + { + ColorMatrix m; + + m.M11 = value1.M11 - value2.M11; + m.M12 = value1.M12 - value2.M12; + m.M13 = value1.M13 - value2.M13; + m.M14 = value1.M14 - value2.M14; + m.M21 = value1.M21 - value2.M21; + m.M22 = value1.M22 - value2.M22; + m.M23 = value1.M23 - value2.M23; + m.M24 = value1.M24 - value2.M24; + m.M31 = value1.M31 - value2.M31; + m.M32 = value1.M32 - value2.M32; + m.M33 = value1.M33 - value2.M33; + m.M34 = value1.M34 - value2.M34; + m.M41 = value1.M41 - value2.M41; + m.M42 = value1.M42 - value2.M42; + m.M43 = value1.M43 - value2.M43; + m.M44 = value1.M44 - value2.M44; + m.M51 = value1.M51 - value2.M51; + m.M52 = value1.M52 - value2.M52; + m.M53 = value1.M53 - value2.M53; + m.M54 = value1.M54 - value2.M54; + + return m; + } + + /// + /// Returns a new matrix with the negated elements of the given matrix. + /// + /// The source matrix. + /// The negated matrix. + public static unsafe ColorMatrix operator -(ColorMatrix value) + { + ColorMatrix m; + + m.M11 = -value.M11; + m.M12 = -value.M12; + m.M13 = -value.M13; + m.M14 = -value.M14; + m.M21 = -value.M21; + m.M22 = -value.M22; + m.M23 = -value.M23; + m.M24 = -value.M24; + m.M31 = -value.M31; + m.M32 = -value.M32; + m.M33 = -value.M33; + m.M34 = -value.M34; + m.M41 = -value.M41; + m.M42 = -value.M42; + m.M43 = -value.M43; + m.M44 = -value.M44; + m.M51 = -value.M51; + m.M52 = -value.M52; + m.M53 = -value.M53; + m.M54 = -value.M54; + + return m; + } + + /// + /// Multiplies a matrix by another matrix. + /// + /// The first source matrix. + /// The second source matrix. + /// The result of the multiplication. + public static ColorMatrix operator *(ColorMatrix value1, ColorMatrix value2) + { + ColorMatrix m; + + // First row + m.M11 = (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31) + (value1.M14 * value2.M41); + m.M12 = (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32) + (value1.M14 * value2.M42); + m.M13 = (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33) + (value1.M14 * value2.M43); + m.M14 = (value1.M11 * value2.M14) + (value1.M12 * value2.M24) + (value1.M13 * value2.M34) + (value1.M14 * value2.M44); + + // Second row + m.M21 = (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31) + (value1.M24 * value2.M41); + m.M22 = (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32) + (value1.M24 * value2.M42); + m.M23 = (value1.M21 * value2.M13) + (value1.M22 * value2.M23) + (value1.M23 * value2.M33) + (value1.M24 * value2.M43); + m.M24 = (value1.M21 * value2.M14) + (value1.M22 * value2.M24) + (value1.M23 * value2.M34) + (value1.M24 * value2.M44); + + // Third row + m.M31 = (value1.M31 * value2.M11) + (value1.M32 * value2.M21) + (value1.M33 * value2.M31) + (value1.M34 * value2.M41); + m.M32 = (value1.M31 * value2.M12) + (value1.M32 * value2.M22) + (value1.M33 * value2.M32) + (value1.M34 * value2.M42); + m.M33 = (value1.M31 * value2.M13) + (value1.M32 * value2.M23) + (value1.M33 * value2.M33) + (value1.M34 * value2.M43); + m.M34 = (value1.M31 * value2.M14) + (value1.M32 * value2.M24) + (value1.M33 * value2.M34) + (value1.M34 * value2.M44); + + // Fourth row + m.M41 = (value1.M41 * value2.M11) + (value1.M42 * value2.M21) + (value1.M43 * value2.M31) + (value1.M44 * value2.M41); + m.M42 = (value1.M41 * value2.M12) + (value1.M42 * value2.M22) + (value1.M43 * value2.M32) + (value1.M44 * value2.M42); + m.M43 = (value1.M41 * value2.M13) + (value1.M42 * value2.M23) + (value1.M43 * value2.M33) + (value1.M44 * value2.M43); + m.M44 = (value1.M41 * value2.M14) + (value1.M42 * value2.M24) + (value1.M43 * value2.M34) + (value1.M44 * value2.M44); + + // Fifth row + m.M51 = (value1.M51 * value2.M11) + (value1.M52 * value2.M21) + (value1.M53 * value2.M31) + (value1.M54 * value2.M41) + value2.M51; + m.M52 = (value1.M51 * value2.M12) + (value1.M52 * value2.M22) + (value1.M53 * value2.M32) + (value1.M54 * value2.M52) + value2.M52; + m.M53 = (value1.M51 * value2.M13) + (value1.M52 * value2.M23) + (value1.M53 * value2.M33) + (value1.M54 * value2.M53) + value2.M53; + m.M54 = (value1.M51 * value2.M14) + (value1.M52 * value2.M24) + (value1.M53 * value2.M34) + (value1.M54 * value2.M54) + value2.M54; + + return m; + } + + /// + /// Multiplies a matrix by a scalar value. + /// + /// The source matrix. + /// The scaling factor. + /// The scaled matrix. + public static ColorMatrix operator *(ColorMatrix value1, float value2) + { + ColorMatrix m; + + m.M11 = value1.M11 * value2; + m.M12 = value1.M12 * value2; + m.M13 = value1.M13 * value2; + m.M14 = value1.M14 * value2; + m.M21 = value1.M21 * value2; + m.M22 = value1.M22 * value2; + m.M23 = value1.M23 * value2; + m.M24 = value1.M24 * value2; + m.M31 = value1.M31 * value2; + m.M32 = value1.M32 * value2; + m.M33 = value1.M33 * value2; + m.M34 = value1.M34 * value2; + m.M41 = value1.M41 * value2; + m.M42 = value1.M42 * value2; + m.M43 = value1.M43 * value2; + m.M44 = value1.M44 * value2; + m.M51 = value1.M51 * value2; + m.M52 = value1.M52 * value2; + m.M53 = value1.M53 * value2; + m.M54 = value1.M54 * value2; + + return m; + } + + /// + /// Returns a boolean indicating whether the given two matrices are equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are equal; False otherwise. + public static bool operator ==(ColorMatrix value1, ColorMatrix value2) => value1.Equals(value2); + + /// + /// Returns a boolean indicating whether the given two matrices are not equal. + /// + /// The first matrix to compare. + /// The second matrix to compare. + /// True if the given matrices are equal; False otherwise. + public static bool operator !=(ColorMatrix value1, ColorMatrix value2) => !value1.Equals(value2); + + /// + public override bool Equals(object obj) => obj is ColorMatrix matrix && this.Equals(matrix); + + /// + public bool Equals(ColorMatrix other) => + this.M11 == other.M11 + && this.M12 == other.M12 + && this.M13 == other.M13 + && this.M14 == other.M14 + && this.M21 == other.M21 + && this.M22 == other.M22 + && this.M23 == other.M23 + && this.M24 == other.M24 + && this.M31 == other.M31 + && this.M32 == other.M32 + && this.M33 == other.M33 + && this.M34 == other.M34 + && this.M41 == other.M41 + && this.M42 == other.M42 + && this.M43 == other.M43 + && this.M44 == other.M44 + && this.M51 == other.M51 + && this.M52 == other.M52 + && this.M53 == other.M53 + && this.M54 == other.M54; + + /// + public override int GetHashCode() + { + HashCode hash = default; + hash.Add(this.M11); + hash.Add(this.M12); + hash.Add(this.M13); + hash.Add(this.M14); + hash.Add(this.M21); + hash.Add(this.M22); + hash.Add(this.M23); + hash.Add(this.M24); + hash.Add(this.M31); + hash.Add(this.M32); + hash.Add(this.M33); + hash.Add(this.M34); + hash.Add(this.M41); + hash.Add(this.M42); + hash.Add(this.M43); + hash.Add(this.M44); + hash.Add(this.M51); + hash.Add(this.M52); + hash.Add(this.M53); + hash.Add(this.M54); + return hash.ToHashCode(); + } + + /// + public override string ToString() + { + CultureInfo ci = CultureInfo.CurrentCulture; + + return string.Format(ci, "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}} {{M41:{12} M42:{13} M43:{14} M44:{15}}} {{M51:{16} M52:{17} M53:{18} M54:{19}}} }}", + this.M11.ToString(ci), this.M12.ToString(ci), this.M13.ToString(ci), this.M14.ToString(ci), + this.M21.ToString(ci), this.M22.ToString(ci), this.M23.ToString(ci), this.M24.ToString(ci), + this.M31.ToString(ci), this.M32.ToString(ci), this.M33.ToString(ci), this.M34.ToString(ci), + this.M41.ToString(ci), this.M42.ToString(ci), this.M43.ToString(ci), this.M44.ToString(ci), + this.M51.ToString(ci), this.M52.ToString(ci), this.M53.ToString(ci), this.M54.ToString(ci)); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/FilterExtensions.cs b/src/ImageSharp/Processing/FilterExtensions.cs index bfae4ae65..70ac23286 100644 --- a/src/ImageSharp/Processing/FilterExtensions.cs +++ b/src/ImageSharp/Processing/FilterExtensions.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing.Processors.Filters; using SixLabors.Primitives; @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Processing /// The image this method extends. /// The filter color matrix /// The . - public static IImageProcessingContext Filter(this IImageProcessingContext source, Matrix4x4 matrix) + public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix) where TPixel : struct, IPixel => source.ApplyProcessor(new FilterProcessor(matrix)); @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing /// The structure that specifies the portion of the image object to alter. /// /// The . - public static IImageProcessingContext Filter(this IImageProcessingContext source, Matrix4x4 matrix, Rectangle rectangle) + public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix, Rectangle rectangle) where TPixel : struct, IPixel => source.ApplyProcessor(new FilterProcessor(matrix), rectangle); } diff --git a/src/ImageSharp/Processing/KnownFilterMatrices.cs b/src/ImageSharp/Processing/KnownFilterMatrices.cs index cf0d19ff8..9c725d027 100644 --- a/src/ImageSharp/Processing/KnownFilterMatrices.cs +++ b/src/ImageSharp/Processing/KnownFilterMatrices.cs @@ -2,19 +2,28 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; - +using SixLabors.ImageSharp.Primitives; + +// Many of these matrices are tranlated from Chromium project where +// SkScalar[] is memory-mapped to a row-major matrix. +// The following translates to our column-major form: +// +// | 0| 1| 2| 3| 4| |0|5|10|15| |M11|M12|M13|M14| +// | 5| 6| 7| 8| 9| |1|6|11|16| |M21|M22|M23|M24| +// |10|11|12|13|14| = |2|7|12|17| = |M31|M32|M33|M34| +// |15|16|17|18|19| |3|8|13|18| |M41|M42|M43|M44| +// |4|9|14|19| |M51|M52|M53|M54| namespace SixLabors.ImageSharp.Processing { /// - /// A collection of known values for composing filters + /// A collection of known values for composing filters /// public static class KnownFilterMatrices { /// /// Gets a filter recreating Achromatomaly (Color desensitivity) color blindness /// - public static Matrix4x4 AchromatomalyFilter { get; } = new Matrix4x4 + public static ColorMatrix AchromatomalyFilter { get; } = new ColorMatrix { M11 = .618F, M12 = .163F, @@ -31,7 +40,7 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets a filter recreating Achromatopsia (Monochrome) color blindness. /// - public static Matrix4x4 AchromatopsiaFilter { get; } = new Matrix4x4 + public static ColorMatrix AchromatopsiaFilter { get; } = new ColorMatrix { M11 = .299F, M12 = .299F, @@ -42,97 +51,97 @@ namespace SixLabors.ImageSharp.Processing M31 = .114F, M32 = .114F, M33 = .114F, - M44 = 1 + M44 = 1F }; /// /// Gets a filter recreating Deuteranomaly (Green-Weak) color blindness. /// - public static Matrix4x4 DeuteranomalyFilter { get; } = new Matrix4x4 + public static ColorMatrix DeuteranomalyFilter { get; } = new ColorMatrix { - M11 = 0.8F, - M12 = 0.258F, - M21 = 0.2F, - M22 = 0.742F, - M23 = 0.142F, - M33 = 0.858F, - M44 = 1 + M11 = .8F, + M12 = .258F, + M21 = .2F, + M22 = .742F, + M23 = .142F, + M33 = .858F, + M44 = 1F }; /// /// Gets a filter recreating Deuteranopia (Green-Blind) color blindness. /// - public static Matrix4x4 DeuteranopiaFilter { get; } = new Matrix4x4 + public static ColorMatrix DeuteranopiaFilter { get; } = new ColorMatrix { - M11 = 0.625F, - M12 = 0.7F, - M21 = 0.375F, - M22 = 0.3F, - M23 = 0.3F, - M33 = 0.7F, - M44 = 1 + M11 = .625F, + M12 = .7F, + M21 = .375F, + M22 = .3F, + M23 = .3F, + M33 = .7F, + M44 = 1F }; /// /// Gets a filter recreating Protanomaly (Red-Weak) color blindness. /// - public static Matrix4x4 ProtanomalyFilter { get; } = new Matrix4x4 + public static ColorMatrix ProtanomalyFilter { get; } = new ColorMatrix { - M11 = 0.817F, - M12 = 0.333F, - M21 = 0.183F, - M22 = 0.667F, - M23 = 0.125F, - M33 = 0.875F, - M44 = 1 + M11 = .817F, + M12 = .333F, + M21 = .183F, + M22 = .667F, + M23 = .125F, + M33 = .875F, + M44 = 1F }; /// /// Gets a filter recreating Protanopia (Red-Blind) color blindness. /// - public static Matrix4x4 ProtanopiaFilter { get; } = new Matrix4x4 + public static ColorMatrix ProtanopiaFilter { get; } = new ColorMatrix { - M11 = 0.567F, - M12 = 0.558F, - M21 = 0.433F, - M22 = 0.442F, - M23 = 0.242F, - M33 = 0.758F, - M44 = 1 + M11 = .567F, + M12 = .558F, + M21 = .433F, + M22 = .442F, + M23 = .242F, + M33 = .758F, + M44 = 1F }; /// /// Gets a filter recreating Tritanomaly (Blue-Weak) color blindness. /// - public static Matrix4x4 TritanomalyFilter { get; } = new Matrix4x4 + public static ColorMatrix TritanomalyFilter { get; } = new ColorMatrix { - M11 = 0.967F, - M21 = 0.33F, - M22 = 0.733F, - M23 = 0.183F, - M32 = 0.267F, - M33 = 0.817F, - M44 = 1 + M11 = .967F, + M21 = .33F, + M22 = .733F, + M23 = .183F, + M32 = .267F, + M33 = .817F, + M44 = 1F }; /// /// Gets a filter recreating Tritanopia (Blue-Blind) color blindness. /// - public static Matrix4x4 TritanopiaFilter { get; } = new Matrix4x4 + public static ColorMatrix TritanopiaFilter { get; } = new ColorMatrix { - M11 = 0.95F, - M21 = 0.05F, - M22 = 0.433F, - M23 = 0.475F, - M32 = 0.567F, - M33 = 0.525F, - M44 = 1 + M11 = .95F, + M21 = .05F, + M22 = .433F, + M23 = .475F, + M32 = .567F, + M33 = .525F, + M44 = 1F }; /// /// Gets an approximated black and white filter /// - public static Matrix4x4 BlackWhiteFilter { get; } = new Matrix4x4() + public static ColorMatrix BlackWhiteFilter { get; } = new ColorMatrix() { M11 = 1.5F, M12 = 1.5F, @@ -143,24 +152,24 @@ namespace SixLabors.ImageSharp.Processing M31 = 1.5F, M32 = 1.5F, M33 = 1.5F, - M41 = -1F, - M42 = -1F, - M43 = -1F, - M44 = 1 + M44 = 1F, + M51 = -1F, + M52 = -1F, + M53 = -1F, }; /// /// Gets a filter recreating an old Kodachrome camera effect. /// - public static Matrix4x4 KodachromeFilter { get; } = new Matrix4x4 + public static ColorMatrix KodachromeFilter { get; } = new ColorMatrix { - M11 = 0.7297023F, - M22 = 0.6109577F, - M33 = 0.597218F, - M41 = 0.105F, - M42 = 0.145F, - M43 = 0.155F, - M44 = 1 + M11 = .7297023F, + M22 = .6109577F, + M33 = .597218F, + M44 = 1F, + M51 = .105F, + M52 = .145F, + M53 = .155F, } * CreateSaturateFilter(1.2F) * CreateContrastFilter(1.35F); @@ -168,15 +177,15 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets a filter recreating an old Lomograph camera effect. /// - public static Matrix4x4 LomographFilter { get; } = new Matrix4x4 + public static ColorMatrix LomographFilter { get; } = new ColorMatrix { M11 = 1.5F, M22 = 1.45F, M33 = 1.16F, - M41 = -.1F, - M42 = -.02F, - M43 = -.07F, - M44 = 1 + M44 = 1F, + M51 = -.1F, + M52 = -.02F, + M53 = -.07F, } * CreateSaturateFilter(1.1F) * CreateContrastFilter(1.33F); @@ -184,21 +193,21 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets a filter recreating an old Polaroid camera effect. /// - public static Matrix4x4 PolaroidFilter { get; } = new Matrix4x4 + public static ColorMatrix PolaroidFilter { get; } = new ColorMatrix { M11 = 1.538F, - M12 = -0.062F, - M13 = -0.262F, - M21 = -0.022F, + M12 = -.062F, + M13 = -.262F, + M21 = -.022F, M22 = 1.578F, - M23 = -0.022F, + M23 = -.022F, M31 = .216F, M32 = -.16F, M33 = 1.5831F, - M41 = 0.02F, - M42 = -0.05F, - M43 = -0.05F, - M44 = 1 + M44 = 1F, + M51 = .02F, + M52 = -.05F, + M53 = -.05F }; /// @@ -209,18 +218,18 @@ namespace SixLabors.ImageSharp.Processing /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. /// /// The proportion of the conversion. Must be greater than or equal to 0. - /// The - public static Matrix4x4 CreateBrightnessFilter(float amount) + /// The + public static ColorMatrix CreateBrightnessFilter(float amount) { Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc - return new Matrix4x4 + return new ColorMatrix { M11 = amount, M22 = amount, M33 = amount, - M44 = 1 + M44 = 1F }; } @@ -232,23 +241,23 @@ namespace SixLabors.ImageSharp.Processing /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. /// /// The proportion of the conversion. Must be greater than or equal to 0. - /// The - public static Matrix4x4 CreateContrastFilter(float amount) + /// The + public static ColorMatrix CreateContrastFilter(float amount) { Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc float contrast = (-.5F * amount) + .5F; - return new Matrix4x4 + return new ColorMatrix { M11 = amount, M22 = amount, M33 = amount, - M41 = contrast, - M42 = contrast, - M43 = contrast, - M44 = 1 + M44 = 1F, + M51 = contrast, + M52 = contrast, + M53 = contrast }; } @@ -257,26 +266,27 @@ namespace SixLabors.ImageSharp.Processing /// /// /// The proportion of the conversion. Must be between 0 and 1. - /// The - public static Matrix4x4 CreateGrayscaleBt601Filter(float amount) + /// The + public static ColorMatrix CreateGrayscaleBt601Filter(float amount) { - Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1F, nameof(amount)); amount = 1F - amount; - // https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc - return new Matrix4x4 - { - M11 = .299F + (.701F * amount), - M12 = .299F - (.299F * amount), - M13 = .299F - (.299F * amount), - M21 = .587F - (.587F * amount), - M22 = .587F + (.413F * amount), - M23 = .587F - (.587F * amount), - M31 = .114F - (.114F * amount), - M32 = .114F - (.114F * amount), - M33 = .114F + (.886F * amount), - M44 = 1 - }; + ColorMatrix m = default; + m.M11 = .299F + (.701F * amount); + m.M21 = .587F - (.587F * amount); + m.M31 = 1F - (m.M11 + m.M21); + + m.M12 = .299F - (.299F * amount); + m.M22 = .587F + (.2848F * amount); + m.M32 = 1F - (m.M12 + m.M22); + + m.M13 = .299F - (.299F * amount); + m.M23 = .587F - (.587F * amount); + m.M33 = 1F - (m.M13 + m.M23); + m.M44 = 1F; + + return m; } /// @@ -284,34 +294,36 @@ namespace SixLabors.ImageSharp.Processing /// /// /// The proportion of the conversion. Must be between 0 and 1. - /// The - public static Matrix4x4 CreateGrayscaleBt709Filter(float amount) + /// The + public static ColorMatrix CreateGrayscaleBt709Filter(float amount) { - Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1F, nameof(amount)); amount = 1F - amount; // https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc - return new Matrix4x4 - { - M11 = .2126F + (.7874F * amount), - M12 = .2126F - (.2126F * amount), - M13 = .2126F - (.2126F * amount), - M21 = .7152F - (.7152F * amount), - M22 = .7152F + (.2848F * amount), - M23 = .7152F - (.7152F * amount), - M31 = .0722F - (.0722F * amount), - M32 = .0722F - (.0722F * amount), - M33 = .0722F + (.9278F * amount), - M44 = 1 - }; + ColorMatrix m = default; + m.M11 = .2126F + (.7874F * amount); + m.M21 = .7152F - (.7152F * amount); + m.M31 = 1F - (m.M11 + m.M21); + + m.M12 = .2126F - (.2126F * amount); + m.M22 = .7152F + (.2848F * amount); + m.M32 = 1F - (m.M12 + m.M22); + + m.M13 = .2126F - (.2126F * amount); + m.M23 = .7152F - (.7152F * amount); + m.M33 = 1F - (m.M13 + m.M23); + m.M44 = 1F; + + return m; } /// /// Create a hue filter matrix using the given angle in degrees. /// /// The angle of rotation in degrees. - /// The - public static Matrix4x4 CreateHueFilter(float degrees) + /// The + public static ColorMatrix CreateHueFilter(float degrees) { // Wrap the angle round at 360. degrees %= 360; @@ -329,18 +341,20 @@ namespace SixLabors.ImageSharp.Processing // The matrix is set up to preserve the luminance of the image. // See http://graficaobscura.com/matrix/index.html // Number are taken from https://msdn.microsoft.com/en-us/library/jj192162(v=vs.85).aspx - return new Matrix4x4 + return new ColorMatrix { M11 = .213F + (cosRadian * .787F) - (sinRadian * .213F), - M12 = .213F - (cosRadian * .213F) - (sinRadian * 0.143F), - M13 = .213F - (cosRadian * .213F) - (sinRadian * .787F), M21 = .715F - (cosRadian * .715F) - (sinRadian * .715F), - M22 = .715F + (cosRadian * .285F) + (sinRadian * 0.140F), - M23 = .715F - (cosRadian * .715F) + (sinRadian * .715F), M31 = .072F - (cosRadian * .072F) + (sinRadian * .928F), - M32 = .072F - (cosRadian * .072F) - (sinRadian * 0.283F), + + M12 = .213F - (cosRadian * .213F) + (sinRadian * .143F), + M22 = .715F + (cosRadian * .285F) + (sinRadian * .140F), + M32 = .072F - (cosRadian * .072F) - (sinRadian * .283F), + + M13 = .213F - (cosRadian * .213F) - (sinRadian * .787F), + M23 = .715F - (cosRadian * .715F) + (sinRadian * .715F), M33 = .072F + (cosRadian * .928F) + (sinRadian * .072F), - M44 = 1 + M44 = 1F }; } @@ -348,23 +362,23 @@ namespace SixLabors.ImageSharp.Processing /// Create an invert filter matrix using the given amount. /// /// The proportion of the conversion. Must be between 0 and 1. - /// The - public static Matrix4x4 CreateInvertFilter(float amount) + /// The + public static ColorMatrix CreateInvertFilter(float amount) { Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc float invert = 1F - (2F * amount); - return new Matrix4x4 + return new ColorMatrix { M11 = invert, M22 = invert, M33 = invert, - M41 = amount, - M42 = amount, - M43 = amount, - M44 = 1 + M44 = 1F, + M51 = amount, + M52 = amount, + M53 = amount, }; } @@ -372,17 +386,17 @@ namespace SixLabors.ImageSharp.Processing /// Create an opacity filter matrix using the given amount. /// /// The proportion of the conversion. Must be between 0 and 1. - /// The - public static Matrix4x4 CreateOpacityFilter(float amount) + /// The + public static ColorMatrix CreateOpacityFilter(float amount) { Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc - return new Matrix4x4 + return new ColorMatrix { - M11 = 1, - M22 = 1, - M33 = 1, + M11 = 1F, + M22 = 1F, + M33 = 1F, M44 = amount }; } @@ -395,25 +409,27 @@ namespace SixLabors.ImageSharp.Processing /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results ///
/// The proportion of the conversion. Must be greater than or equal to 0. - /// The - public static Matrix4x4 CreateSaturateFilter(float amount) + /// The + public static ColorMatrix CreateSaturateFilter(float amount) { Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc - return new Matrix4x4 - { - M11 = .213F + (.787F * amount), - M12 = .213F - (.213F * amount), - M13 = .213F - (.213F * amount), - M21 = .715F - (.715F * amount), - M22 = .715F + (.285F * amount), - M23 = .715F - (.715F * amount), - M31 = 1F - ((.213F + (.787F * amount)) + (.715F - (.715F * amount))), - M32 = 1F - ((.213F - (.213F * amount)) + (.715F + (.285F * amount))), - M33 = 1F - ((.213F - (.213F * amount)) + (.715F - (.715F * amount))), - M44 = 1 - }; + ColorMatrix m = default; + m.M11 = .213F + (.787F * amount); + m.M21 = .715F - (.715F * amount); + m.M31 = 1F - (m.M11 + m.M21); + + m.M12 = .213F - (.213F * amount); + m.M22 = .715F + (.285F * amount); + m.M32 = 1F - (m.M12 + m.M22); + + m.M13 = .213F - (.213F * amount); + m.M23 = .715F - (.715F * amount); + m.M33 = 1F - (m.M13 + m.M23); + m.M44 = 1F; + + return m; } /// @@ -421,25 +437,27 @@ namespace SixLabors.ImageSharp.Processing /// The formula used matches the svg specification. /// /// The proportion of the conversion. Must be between 0 and 1. - /// The - public static Matrix4x4 CreateSepiaFilter(float amount) + /// The + public static ColorMatrix CreateSepiaFilter(float amount) { Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); amount = 1F - amount; // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc - return new Matrix4x4 + return new ColorMatrix { M11 = .393F + (.607F * amount), - M12 = .349F - (.349F * amount), - M13 = .272F - (.272F * amount), M21 = .769F - (.769F * amount), - M22 = .686F + (.314F * amount), - M23 = .534F - (.534F * amount), M31 = .189F - (.189F * amount), + + M12 = .349F - (.349F * amount), + M22 = .686F + (.314F * amount), M32 = .168F - (.168F * amount), + + M13 = .272F - (.272F * amount), + M23 = .534F - (.534F * amount), M33 = .131F + (.869F * amount), - M44 = 1 + M44 = 1F }; } } diff --git a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs index d3a44c066..d6a32d889 100644 --- a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs @@ -3,16 +3,16 @@ using System; using System.Numerics; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Primitives; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Filters { /// - /// Provides methods that accept a matrix to apply free-form filters to images. + /// Provides methods that accept a matrix to apply free-form filters to images. /// /// The pixel format. internal class FilterProcessor : ImageProcessor @@ -22,38 +22,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters /// Initializes a new instance of the class. /// /// The matrix used to apply the image filter - public FilterProcessor(Matrix4x4 matrix) - { - this.Matrix = matrix; - } + public FilterProcessor(ColorMatrix matrix) => this.Matrix = matrix; /// - /// Gets the used to apply the image filter. + /// Gets the used to apply the image filter. /// - public Matrix4x4 Matrix { get; } + public ColorMatrix Matrix { get; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startX = interest.X; - Matrix4x4 matrix = this.Matrix; + ColorMatrix matrix = this.Matrix; - ParallelHelper.IterateRows( + ParallelHelper.IterateRowsWithTempBuffer( interest, configuration, - rows => + (rows, vectorBuffer) => { for (int y = rows.Min; y < rows.Max; y++) { - Span row = source.GetPixelRowSpan(y); - - for (int x = interest.X; x < interest.Right; x++) - { - ref TPixel pixel = ref row[x]; - var vector = Vector4.Transform(pixel.ToVector4(), matrix); - pixel.FromVector4(vector); - } + Span vectorSpan = vectorBuffer.Span; + int length = vectorSpan.Length; + Span rowSpan = source.GetPixelRowSpan(y).Slice(startX, length); + PixelOperations.Instance.ToVector4(configuration, rowSpan, vectorSpan); + + Vector4Utils.Transform(vectorSpan, ref matrix); + + PixelOperations.Instance.FromVector4(configuration, vectorSpan, rowSpan); } }); } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/YCbCrColorConversion.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/YCbCrColorConversion.cs index 05edd2791..8417b32f2 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/YCbCrColorConversion.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/YCbCrColorConversion.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg { var values = new JpegColorConverter.ComponentValues(this.input, 0); - JpegColorConverter.FromYCbCrBasic.ConvertCore(values, this.output); + JpegColorConverter.FromYCbCrBasic.ConvertCore(values, this.output, 255F, 128F); } [Benchmark] @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg { var values = new JpegColorConverter.ComponentValues(this.input, 0); - JpegColorConverter.FromYCbCrSimd.ConvertCore(values, this.output); + JpegColorConverter.FromYCbCrSimd.ConvertCore(values, this.output, 255F, 128F); } [Benchmark] @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg { var values = new JpegColorConverter.ComponentValues(this.input, 0); - JpegColorConverter.FromYCbCrSimdAvx2.ConvertCore(values, this.output); + JpegColorConverter.FromYCbCrSimdAvx2.ConvertCore(values, this.output, 255F, 128F); } private static Buffer2D[] CreateRandomValues( diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 251475567..0ebfbf311 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -4,6 +4,9 @@ using System.IO; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + using Xunit; // ReSharper disable InconsistentNaming @@ -19,12 +22,14 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] AllBmpFiles = All; + public static readonly string[] BitfieldsBmpFiles = BitFields; + public static readonly TheoryData RatioFiles = new TheoryData { { TestImages.Bmp.Car, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter }, { TestImages.Bmp.V5Header, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter }, - { TestImages.Bmp.RLE, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } + { TestImages.Bmp.RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } }; [Theory] @@ -34,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage(new BmpDecoder())) { - image.DebugSave(provider, "bmp"); + image.DebugSave(provider); if (TestEnvironment.IsWindows) { @@ -44,17 +49,47 @@ namespace SixLabors.ImageSharp.Tests } [Theory] - [WithFile(F, CommonNonDefaultPixelTypes)] - public void BmpDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider) + [WithFileCollection(nameof(BitfieldsBmpFiles), PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeBitfields(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage(new BmpDecoder())) { - image.DebugSave(provider, "bmp"); + image.DebugSave(provider); image.CompareToOriginal(provider); } } + [Theory] + [WithFile(Bit32Rgba, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeBitmap_WithAlphaChannel(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, new MagickReferenceDecoder()); + } + } + + [Theory] + [WithFile(Rgba321010102, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeBitfields_WithUnusualBitmasks(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + + // Choosing large tolerance of 6.1 here, because for some reason with the MagickReferenceDecoder the alpha channel + // seems to be wrong. This bitmap has an alpha channel of two bits. In many cases this alpha channel has a value of 3, + // which should be remapped to 255 for RGBA32, but the magick decoder has a value of 191 set. + // The total difference without the alpha channel is still: 0.0204% + // Exporting the image as PNG with GIMP yields to the same result as the imagesharp implementation. + image.CompareToOriginal(provider, ImageComparer.TolerantPercentage(6.1f), new MagickReferenceDecoder()); + } + } + [Theory] [WithFile(WinBmpv2, PixelTypes.Rgba32)] public void BmpDecoder_CanDecodeBmpv2(TestImageProvider provider) @@ -62,7 +97,67 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage(new BmpDecoder())) { - image.DebugSave(provider, "png"); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + + [Theory] + [WithFile(WinBmpv3, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeBmpv3(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + + [Theory] + [WithFile(Rgba32bf56, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeAdobeBmpv3(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, new MagickReferenceDecoder()); + } + } + + [Theory] + [WithFile(WinBmpv4, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeBmpv4(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + + [Theory] + [WithFile(WinBmpv5, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecodeBmpv5(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + + [Theory] + [WithFile(F, CommonNonDefaultPixelTypes)] + public void BmpDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); image.CompareToOriginal(provider); } } @@ -74,7 +169,7 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage(new BmpDecoder())) { - image.DebugSave(provider, "png"); + image.DebugSave(provider); image.CompareToOriginal(provider); } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index b9f855cf1..f818be8a9 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests { { TestImages.Bmp.Car, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter }, { TestImages.Bmp.V5Header, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter }, - { TestImages.Bmp.RLE, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } + { TestImages.Bmp.RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } }; public static readonly TheoryData BmpBitsPerPixelFiles = diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 158a085d5..aed1edfbf 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -57,8 +57,7 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = file.CreateImage()) { - var encoder = new PngEncoder { Quantizer = new WuQuantizer(KnownDiffusers.JarvisJudiceNinke, 256), ColorType = PngColorType.Palette }; - image.Save($"{path}/{file.FileName}.png", encoder); + image.Save($"{path}/{file.FileName}"); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 81c76390c..7e7218c9d 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -228,7 +228,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.PrintLinearData(input); Block8x8F dest = block; - dest.NormalizeColorsInplace(); + dest.NormalizeColorsInplace(255); float[] array = new float[64]; dest.CopyTo(array); @@ -253,11 +253,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Block8x8F source = CreateRandomFloatBlock(-200, 200, seed); Block8x8F expected = source; - expected.NormalizeColorsInplace(); + expected.NormalizeColorsInplace(255); expected.RoundInplace(); Block8x8F actual = source; - actual.NormalizeColorsAndRoundInplaceAvx2(); + actual.NormalizeColorsAndRoundInplaceAvx2(255); this.Output.WriteLine(expected.ToString()); this.Output.WriteLine(actual.ToString()); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 8e30eb9e5..caaad73c9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void ConvertFromYCbCrBasic(int inputBufferLength, int resultBufferLength, int seed) { ValidateRgbToYCbCrConversion( - new JpegColorConverter.FromYCbCrBasic(), + new JpegColorConverter.FromYCbCrBasic(8), 3, inputBufferLength, resultBufferLength, @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg JpegColorConverter.ComponentValues values = CreateRandomValues(3, size, seed); var result = new Vector4[size]; - JpegColorConverter.FromYCbCrSimd.ConvertCore(values, result); + JpegColorConverter.FromYCbCrSimd.ConvertCore(values, result, 255, 128); for (int i = 0; i < size; i++) { @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void FromYCbCrSimd(int inputBufferLength, int resultBufferLength, int seed) { ValidateRgbToYCbCrConversion( - new JpegColorConverter.FromYCbCrSimd(), + new JpegColorConverter.FromYCbCrSimd(8), 3, inputBufferLength, resultBufferLength, @@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg //JpegColorConverter.FromYCbCrSimdAvx2.LogPlz = s => this.Output.WriteLine(s); ValidateRgbToYCbCrConversion( - new JpegColorConverter.FromYCbCrSimdAvx2(), + new JpegColorConverter.FromYCbCrSimdAvx2(8), 3, inputBufferLength, resultBufferLength, @@ -140,7 +140,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg JpegColorConverter.ComponentValues values = CreateRandomValues(3, count, 1); var result = new Vector4[count]; - JpegColorConverter converter = simd ? (JpegColorConverter)new JpegColorConverter.FromYCbCrSimd() : new JpegColorConverter.FromYCbCrBasic(); + JpegColorConverter converter = simd ? (JpegColorConverter)new JpegColorConverter.FromYCbCrSimd(8) : new JpegColorConverter.FromYCbCrBasic(8); // Warm up: converter.ConvertToRgba(values, result); @@ -161,7 +161,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var v = new Vector4(0, 0, 0, 1F); var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); - var converter = JpegColorConverter.GetConverter(JpegColorSpace.Cmyk); + var converter = JpegColorConverter.GetConverter(JpegColorSpace.Cmyk, 8); JpegColorConverter.ComponentValues values = CreateRandomValues(4, inputBufferLength, seed); var result = new Vector4[resultBufferLength]; @@ -194,7 +194,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [MemberData(nameof(CommonConversionData))] public void ConvertFromGrayScale(int inputBufferLength, int resultBufferLength, int seed) { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.Grayscale); + var converter = JpegColorConverter.GetConverter(JpegColorSpace.Grayscale, 8); JpegColorConverter.ComponentValues values = CreateRandomValues(1, inputBufferLength, seed); var result = new Vector4[resultBufferLength]; @@ -216,7 +216,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [MemberData(nameof(CommonConversionData))] public void ConvertFromRgb(int inputBufferLength, int resultBufferLength, int seed) { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.RGB); + var converter = JpegColorConverter.GetConverter(JpegColorSpace.RGB, 8); JpegColorConverter.ComponentValues values = CreateRandomValues(3, inputBufferLength, seed); var result = new Vector4[resultBufferLength]; @@ -243,7 +243,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var v = new Vector4(0, 0, 0, 1F); var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); - var converter = JpegColorConverter.GetConverter(JpegColorSpace.Ycck); + var converter = JpegColorConverter.GetConverter(JpegColorSpace.Ycck, 8); JpegColorConverter.ComponentValues values = CreateRandomValues(4, inputBufferLength, seed); var result = new Vector4[resultBufferLength]; @@ -308,7 +308,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg int seed) { ValidateRgbToYCbCrConversion( - JpegColorConverter.GetConverter(colorSpace), + JpegColorConverter.GetConverter(colorSpace,8), componentCount, inputBufferLength, resultBufferLength, diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index 73167a4b7..1c9d207cd 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -42,5 +42,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // TODO: We need a public ImageDecoderException class in ImageSharp! Assert.ThrowsAny(() => provider.GetImage(JpegDecoder)); } + + [Theory] + [WithFile(TestImages.Jpeg.Issues.InvalidJpegThrowsWrongException797, PixelTypes.Rgba32)] + public void LoadingImage_InvalidTagLength_ShouldThrow(TestImageProvider provider) + where TPixel : struct, IPixel => Assert.Throws(() => provider.GetImage()); + + [Theory] + [WithFile(TestImages.Jpeg.Issues.AccessViolationException798, PixelTypes.Rgba32)] + public void LoadingImage_BadHuffman_ShouldNotThrow(TestImageProvider provider) + where TPixel : struct, IPixel => Assert.NotNull(provider.GetImage()); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index 40de25b30..03f1826ed 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -28,7 +28,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.ExifResizeOutOfRange696, TestImages.Jpeg.Issues.InvalidAPP0721, TestImages.Jpeg.Issues.ExifGetString750Load, - TestImages.Jpeg.Issues.ExifGetString750Transform + TestImages.Jpeg.Issues.ExifGetString750Transform, + + // High depth images + TestImages.Jpeg.Baseline.Testorig12bit, }; public static string[] ProgressiveTestJpegs = diff --git a/tests/ImageSharp.Tests/Helpers/Vector4UtilsTests.cs b/tests/ImageSharp.Tests/Helpers/Vector4UtilsTests.cs index 9416be740..f2e98b131 100644 --- a/tests/ImageSharp.Tests/Helpers/Vector4UtilsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/Vector4UtilsTests.cs @@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Tests.Helpers { public class Vector4UtilsTests { + private readonly ApproximateFloatComparer ApproximateFloatComparer = new ApproximateFloatComparer(1e-6f); + [Theory] [InlineData(0)] [InlineData(1)] @@ -23,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers Vector4Utils.Premultiply(source); - Assert.Equal(expected, source, new ApproximateFloatComparer(1e-6f)); + Assert.Equal(expected, source, this.ApproximateFloatComparer); } [Theory] @@ -38,7 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers Vector4Utils.UnPremultiply(source); - Assert.Equal(expected, source, new ApproximateFloatComparer(1e-6f)); + Assert.Equal(expected, source, this.ApproximateFloatComparer); } } } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 7452d6e49..e20f1514c 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -27,7 +27,7 @@ - + diff --git a/tests/ImageSharp.Tests/Primitives/ColorMatrixTests.cs b/tests/ImageSharp.Tests/Primitives/ColorMatrixTests.cs new file mode 100644 index 000000000..2fbe260ec --- /dev/null +++ b/tests/ImageSharp.Tests/Primitives/ColorMatrixTests.cs @@ -0,0 +1,270 @@ +using System; +using System.Globalization; +using SixLabors.ImageSharp.Primitives; +using SixLabors.ImageSharp.Processing; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Primitives +{ + public class ColorMatrixTests + { + private readonly ApproximateFloatComparer ApproximateFloatComparer = new ApproximateFloatComparer(1e-6f); + + [Fact] + public void ColorMatrixIdentityIsCorrect() + { + ColorMatrix val = default; + val.M11 = val.M22 = val.M33 = val.M44 = 1F; + + Assert.Equal(val, ColorMatrix.Identity, this.ApproximateFloatComparer); + } + + [Fact] + public void ColorMatrixCanDetectIdentity() + { + ColorMatrix m = ColorMatrix.Identity; + Assert.True(m.IsIdentity); + + m.M12 = 1F; + Assert.False(m.IsIdentity); + } + + [Fact] + public void ColorMatrixEquality() + { + ColorMatrix m = KnownFilterMatrices.CreateHueFilter(45F); + ColorMatrix m2 = KnownFilterMatrices.CreateHueFilter(45F); + object obj = m2; + + Assert.True(m.Equals(obj)); + Assert.True(m.Equals(m2)); + Assert.True(m == m2); + Assert.False(m != m2); + } + + [Fact] + public void ColorMatrixMultiply() + { + ColorMatrix value1 = this.CreateAllTwos(); + ColorMatrix value2 = this.CreateAllThrees(); + + ColorMatrix m; + + // First row + m.M11 = (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31) + (value1.M14 * value2.M41); + m.M12 = (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32) + (value1.M14 * value2.M42); + m.M13 = (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33) + (value1.M14 * value2.M43); + m.M14 = (value1.M11 * value2.M14) + (value1.M12 * value2.M24) + (value1.M13 * value2.M34) + (value1.M14 * value2.M44); + + // Second row + m.M21 = (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31) + (value1.M24 * value2.M41); + m.M22 = (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32) + (value1.M24 * value2.M42); + m.M23 = (value1.M21 * value2.M13) + (value1.M22 * value2.M23) + (value1.M23 * value2.M33) + (value1.M24 * value2.M43); + m.M24 = (value1.M21 * value2.M14) + (value1.M22 * value2.M24) + (value1.M23 * value2.M34) + (value1.M24 * value2.M44); + + // Third row + m.M31 = (value1.M31 * value2.M11) + (value1.M32 * value2.M21) + (value1.M33 * value2.M31) + (value1.M34 * value2.M41); + m.M32 = (value1.M31 * value2.M12) + (value1.M32 * value2.M22) + (value1.M33 * value2.M32) + (value1.M34 * value2.M42); + m.M33 = (value1.M31 * value2.M13) + (value1.M32 * value2.M23) + (value1.M33 * value2.M33) + (value1.M34 * value2.M43); + m.M34 = (value1.M31 * value2.M14) + (value1.M32 * value2.M24) + (value1.M33 * value2.M34) + (value1.M34 * value2.M44); + + // Fourth row + m.M41 = (value1.M41 * value2.M11) + (value1.M42 * value2.M21) + (value1.M43 * value2.M31) + (value1.M44 * value2.M41); + m.M42 = (value1.M41 * value2.M12) + (value1.M42 * value2.M22) + (value1.M43 * value2.M32) + (value1.M44 * value2.M42); + m.M43 = (value1.M41 * value2.M13) + (value1.M42 * value2.M23) + (value1.M43 * value2.M33) + (value1.M44 * value2.M43); + m.M44 = (value1.M41 * value2.M14) + (value1.M42 * value2.M24) + (value1.M43 * value2.M34) + (value1.M44 * value2.M44); + + // Fifth row + m.M51 = (value1.M51 * value2.M11) + (value1.M52 * value2.M21) + (value1.M53 * value2.M31) + (value1.M54 * value2.M41) + value2.M51; + m.M52 = (value1.M51 * value2.M12) + (value1.M52 * value2.M22) + (value1.M53 * value2.M32) + (value1.M54 * value2.M52) + value2.M52; + m.M53 = (value1.M51 * value2.M13) + (value1.M52 * value2.M23) + (value1.M53 * value2.M33) + (value1.M54 * value2.M53) + value2.M53; + m.M54 = (value1.M51 * value2.M14) + (value1.M52 * value2.M24) + (value1.M53 * value2.M34) + (value1.M54 * value2.M54) + value2.M54; + + Assert.Equal(m, value1 * value2, this.ApproximateFloatComparer); + } + + [Fact] + public void ColorMatrixMultiplyScalar() + { + ColorMatrix m = this.CreateAllTwos(); + Assert.Equal(this.CreateAllFours(), m * 2, this.ApproximateFloatComparer); + } + + [Fact] + public void ColorMatrixSubtract() + { + ColorMatrix m = this.CreateAllOnes() + this.CreateAllTwos(); + Assert.Equal(this.CreateAllThrees(), m); + } + + [Fact] + public void ColorMatrixNegate() + { + ColorMatrix m = this.CreateAllOnes() * -1F; + Assert.Equal(m, -this.CreateAllOnes()); + } + + [Fact] + public void ColorMatrixAdd() + { + ColorMatrix m = this.CreateAllOnes() + this.CreateAllTwos(); + Assert.Equal(this.CreateAllThrees(), m); + } + + [Fact] + public void ColorMatrixHashCode() + { +#if NETCOREAPP2_1 + ColorMatrix m = KnownFilterMatrices.CreateBrightnessFilter(.5F); + HashCode hash = default; + hash.Add(m.M11); + hash.Add(m.M12); + hash.Add(m.M13); + hash.Add(m.M14); + hash.Add(m.M21); + hash.Add(m.M22); + hash.Add(m.M23); + hash.Add(m.M24); + hash.Add(m.M31); + hash.Add(m.M32); + hash.Add(m.M33); + hash.Add(m.M34); + hash.Add(m.M41); + hash.Add(m.M42); + hash.Add(m.M43); + hash.Add(m.M44); + hash.Add(m.M51); + hash.Add(m.M52); + hash.Add(m.M53); + hash.Add(m.M54); + + Assert.Equal(hash.ToHashCode(), m.GetHashCode()); +#endif + } + + [Fact] + public void ColorMatrixToString() + { + ColorMatrix m = KnownFilterMatrices.CreateBrightnessFilter(.5F); + + CultureInfo ci = CultureInfo.CurrentCulture; + + string expected = string.Format(ci, "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}} {{M41:{12} M42:{13} M43:{14} M44:{15}}} {{M51:{16} M52:{17} M53:{18} M54:{19}}} }}", + m.M11.ToString(ci), m.M12.ToString(ci), m.M13.ToString(ci), m.M14.ToString(ci), + m.M21.ToString(ci), m.M22.ToString(ci), m.M23.ToString(ci), m.M24.ToString(ci), + m.M31.ToString(ci), m.M32.ToString(ci), m.M33.ToString(ci), m.M34.ToString(ci), + m.M41.ToString(ci), m.M42.ToString(ci), m.M43.ToString(ci), m.M44.ToString(ci), + m.M51.ToString(ci), m.M52.ToString(ci), m.M53.ToString(ci), m.M54.ToString(ci)); + + Assert.Equal(expected, m.ToString()); + } + + private ColorMatrix CreateAllOnes() + { + return new ColorMatrix + { + M11 = 1F, + M12 = 1F, + M13 = 1F, + M14 = 1F, + M21 = 1F, + M22 = 1F, + M23 = 1F, + M24 = 1F, + M31 = 1F, + M32 = 1F, + M33 = 1F, + M34 = 1F, + M41 = 1F, + M42 = 1F, + M43 = 1F, + M44 = 1F, + M51 = 1F, + M52 = 1F, + M53 = 1F, + M54 = 1F + }; + } + + private ColorMatrix CreateAllTwos() + { + return new ColorMatrix + { + M11 = 2F, + M12 = 2F, + M13 = 2F, + M14 = 2F, + M21 = 2F, + M22 = 2F, + M23 = 2F, + M24 = 2F, + M31 = 2F, + M32 = 2F, + M33 = 2F, + M34 = 2F, + M41 = 2F, + M42 = 2F, + M43 = 2F, + M44 = 2F, + M51 = 2F, + M52 = 2F, + M53 = 2F, + M54 = 2F + }; + } + + private ColorMatrix CreateAllThrees() + { + return new ColorMatrix + { + M11 = 3F, + M12 = 3F, + M13 = 3F, + M14 = 3F, + M21 = 3F, + M22 = 3F, + M23 = 3F, + M24 = 3F, + M31 = 3F, + M32 = 3F, + M33 = 3F, + M34 = 3F, + M41 = 3F, + M42 = 3F, + M43 = 3F, + M44 = 3F, + M51 = 3F, + M52 = 3F, + M53 = 3F, + M54 = 3F + }; + } + + private ColorMatrix CreateAllFours() + { + return new ColorMatrix + { + M11 = 4F, + M12 = 4F, + M13 = 4F, + M14 = 4F, + M21 = 4F, + M22 = 4F, + M23 = 4F, + M24 = 4F, + M31 = 4F, + M32 = 4F, + M33 = 4F, + M34 = 4F, + M41 = 4F, + M42 = 4F, + M43 = 4F, + M44 = 4F, + M51 = 4F, + M52 = 4F, + M53 = 4F, + M54 = 4F + }; + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs index ed790cbac..54a8dd4b7 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs @@ -8,10 +8,13 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { using SixLabors.ImageSharp.Processing; + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; [GroupOutput("Filters")] public class BrightnessTest { + private readonly ImageComparer imageComparer = ImageComparer.Tolerant(0.007F); + public static readonly TheoryData BrightnessValues = new TheoryData { @@ -22,9 +25,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects [Theory] [WithTestPatternImages(nameof(BrightnessValues), 48, 48, PixelTypes.Rgba32)] public void ApplyBrightnessFilter(TestImageProvider provider, float value) - where TPixel : struct, IPixel - { - provider.RunValidatingProcessorTest(ctx => ctx.Brightness(value), value); - } + where TPixel : struct, IPixel => provider.RunValidatingProcessorTest(ctx => ctx.Brightness(value), value, this.imageComparer); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs index 3d48e16ec..8ac56655e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs @@ -2,17 +2,19 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { using SixLabors.ImageSharp.Processing; + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; [GroupOutput("Filters")] public class ColorBlindnessTest { + private readonly ImageComparer imageComparer = ImageComparer.Tolerant(0.03F); + public static readonly TheoryData ColorBlindnessFilters = new TheoryData { @@ -29,9 +31,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters [Theory] [WithTestPatternImages(nameof(ColorBlindnessFilters), 48, 48, PixelTypes.Rgba32)] public void ApplyColorBlindnessFilter(TestImageProvider provider, ColorBlindnessMode colorBlindness) - where TPixel : struct, IPixel - { - provider.RunValidatingProcessorTest(x => x.ColorBlindness(colorBlindness), colorBlindness.ToString()); - } + where TPixel : struct, IPixel => provider.RunValidatingProcessorTest(x => x.ColorBlindness(colorBlindness), colorBlindness.ToString(), this.imageComparer); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index 479a3c33a..68daa80ea 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -1,16 +1,13 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; [GroupOutput("Filters")] @@ -25,7 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyFilter(TestImageProvider provider) where TPixel : struct, IPixel { - Matrix4x4 m = CreateCombinedTestFilterMatrix(); + ColorMatrix m = CreateCombinedTestFilterMatrix(); provider.RunValidatingProcessorTest(x => x.Filter(m), comparer: ValidatorComparer); } @@ -35,18 +32,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { - Matrix4x4 m = CreateCombinedTestFilterMatrix(); + ColorMatrix m = CreateCombinedTestFilterMatrix(); provider.RunRectangleConstrainedValidatingProcessorTest((x, b) => x.Filter(m, b), comparer: ValidatorComparer); } - private static Matrix4x4 CreateCombinedTestFilterMatrix() + private static ColorMatrix CreateCombinedTestFilterMatrix() { - Matrix4x4 brightness = KnownFilterMatrices.CreateBrightnessFilter(0.9F); - Matrix4x4 hue = KnownFilterMatrices.CreateHueFilter(180F); - Matrix4x4 saturation = KnownFilterMatrices.CreateSaturateFilter(1.5F); - Matrix4x4 m = brightness * hue * saturation; - return m; + ColorMatrix brightness = KnownFilterMatrices.CreateBrightnessFilter(0.9F); + ColorMatrix hue = KnownFilterMatrices.CreateHueFilter(180F); + ColorMatrix saturation = KnownFilterMatrices.CreateSaturateFilter(1.5F); + return brightness * hue * saturation; } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index af2e2558d..80325df2b 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -136,12 +136,14 @@ namespace SixLabors.ImageSharp.Tests public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg"; public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg"; public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg"; + public const string Testorig12bit = "Jpg/baseline/testorig12.jpg"; public static readonly string[] All = { Cmyk, Ycck, Exif, Floorplan, Calliphora, Turtle, GammaDalaiLamaGray, - Hiyamugi, Jpeg400, Jpeg420Exif, Jpeg444, Ratio1x1 + Hiyamugi, Jpeg400, Jpeg420Exif, Jpeg444, + Ratio1x1, Testorig12bit }; } @@ -166,6 +168,8 @@ namespace SixLabors.ImageSharp.Tests public const string OrderedInterleavedProgressive723C = "Jpg/issues/Issue723-Ordered-Interleaved-Progressive-C.jpg"; public const string ExifGetString750Transform = "Jpg/issues/issue750-exif-tranform.jpg"; public const string ExifGetString750Load = "Jpg/issues/issue750-exif-load.jpg"; + public const string InvalidJpegThrowsWrongException797 = "Jpg/issues/Issue797-InvalidImage.jpg"; + public const string AccessViolationException798 = "Jpg/issues/Issue798-AccessViolationException.jpg"; } public static readonly string[] All = Baseline.All.Concat(Progressive.All).ToArray(); @@ -192,7 +196,8 @@ namespace SixLabors.ImageSharp.Tests public const string NegHeight = "Bmp/neg_height.bmp"; public const string CoreHeader = "Bmp/BitmapCoreHeaderQR.bmp"; public const string V5Header = "Bmp/BITMAPV5HEADER.bmp"; - public const string RLE = "Bmp/RunLengthEncoded.bmp"; + public const string RLE8 = "Bmp/RunLengthEncoded.bmp"; + public const string RLE4 = "Bmp/pal4rle.bmp"; public const string RLEInverted = "Bmp/RunLengthEncoded-inverted.bmp"; public const string Bit1 = "Bmp/pal1.bmp"; public const string Bit1Pal1 = "Bmp/pal1p1.bmp"; @@ -202,19 +207,46 @@ namespace SixLabors.ImageSharp.Tests public const string Bit16 = "Bmp/test16.bmp"; public const string Bit16Inverted = "Bmp/test16-inverted.bmp"; public const string Bit32Rgb = "Bmp/rgb32.bmp"; - + public const string Bit32Rgba = "Bmp/rgba32.bmp"; + // Note: This format can be called OS/2 BMPv1, or Windows BMPv2 public const string WinBmpv2 = "Bmp/pal8os2v1_winv2.bmp"; + + public const string WinBmpv3 = "Bmp/rgb24.bmp"; + public const string WinBmpv4 = "Bmp/pal8v4.bmp"; + public const string WinBmpv5 = "Bmp/pal8v5.bmp"; public const string Bit8Palette4 = "Bmp/pal8-0.bmp"; public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp"; + // Bitmap images with compression type BITFIELDS + public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp"; + public const string Rgb32bf = "Bmp/rgb32bf.bmp"; + public const string Rgb16bfdef = "Bmp/rgb16bfdef.bmp"; + public const string Rgb16565 = "Bmp/rgb16-565.bmp"; + public const string Rgb16565pal = "Bmp/rgb16-565pal.bmp"; + public const string Issue735 = "Bmp/issue735.bmp"; + public const string Rgba32bf56 = "Bmp/rgba32h56.bmp"; + public const string Rgba321010102 = "Bmp/rgba32-1010102.bmp"; + + public static readonly string[] BitFields + = { + Rgb32bfdef, + Rgb32bf, + Rgb16565, + Rgb16bfdef, + Rgb16565pal, + Issue735, + }; + public static readonly string[] All = { Car, F, NegHeight, CoreHeader, - V5Header, RLE, + V5Header, + RLE4, + RLE8, RLEInverted, Bit1, Bit1Pal1, @@ -222,7 +254,8 @@ namespace SixLabors.ImageSharp.Tests Bit8, Bit8Inverted, Bit16, - Bit16Inverted + Bit16Inverted, + Bit32Rgb }; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index 47ca6cccb..872a935ff 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; +using SixLabors.ImageSharp.Primitives; namespace SixLabors.ImageSharp.Tests { @@ -11,8 +12,9 @@ namespace SixLabors.ImageSharp.Tests /// internal readonly struct ApproximateFloatComparer : IEqualityComparer, + IEqualityComparer, IEqualityComparer, - IEqualityComparer + IEqualityComparer { private readonly float Epsilon; @@ -20,7 +22,7 @@ namespace SixLabors.ImageSharp.Tests /// Initializes a new instance of the class. /// /// The comparison error difference epsilon to use. - public ApproximateFloatComparer(float epsilon = 1f) => this.Epsilon = epsilon; + public ApproximateFloatComparer(float epsilon = 1F) => this.Epsilon = epsilon; /// public bool Equals(float x, float y) @@ -34,17 +36,29 @@ namespace SixLabors.ImageSharp.Tests public int GetHashCode(float obj) => obj.GetHashCode(); /// - public bool Equals(Vector4 a, Vector4 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W); + public bool Equals(Vector2 x, Vector2 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y); /// - public int GetHashCode(Vector4 obj) => obj.GetHashCode(); + public int GetHashCode(Vector2 obj) => obj.GetHashCode(); + + /// + public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W); /// - public bool Equals(Vector2 a, Vector2 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y); + public int GetHashCode(Vector4 obj) => obj.GetHashCode(); - public int GetHashCode(Vector2 obj) + /// + public bool Equals(ColorMatrix x, ColorMatrix y) { - throw new System.NotImplementedException(); + return + this.Equals(x.M11, y.M11) && this.Equals(x.M12, y.M12) && this.Equals(x.M13, y.M13) && this.Equals(x.M14, y.M14) + && this.Equals(x.M21, y.M21) && this.Equals(x.M22, y.M22) && this.Equals(x.M23, y.M23) && this.Equals(x.M24, y.M24) + && this.Equals(x.M31, y.M31) && this.Equals(x.M32, y.M32) && this.Equals(x.M33, y.M33) && this.Equals(x.M34, y.M34) + && this.Equals(x.M41, y.M41) && this.Equals(x.M42, y.M42) && this.Equals(x.M43, y.M43) && this.Equals(x.M44, y.M44) + && this.Equals(x.M51, y.M51) && this.Equals(x.M52, y.M52) && this.Equals(x.M53, y.M53) && this.Equals(x.M54, y.M54); } + + /// + public int GetHashCode(ColorMatrix obj) => obj.GetHashCode(); } } \ No newline at end of file diff --git a/tests/Images/Input/Bmp/issue735.bmp b/tests/Images/Input/Bmp/issue735.bmp new file mode 100644 index 000000000..31fadfcf5 Binary files /dev/null and b/tests/Images/Input/Bmp/issue735.bmp differ diff --git a/tests/Images/Input/Bmp/pal4rle.bmp b/tests/Images/Input/Bmp/pal4rle.bmp new file mode 100644 index 000000000..a5672aebd Binary files /dev/null and b/tests/Images/Input/Bmp/pal4rle.bmp differ diff --git a/tests/Images/Input/Bmp/pal8v4.bmp b/tests/Images/Input/Bmp/pal8v4.bmp new file mode 100644 index 000000000..34ebb8030 Binary files /dev/null and b/tests/Images/Input/Bmp/pal8v4.bmp differ diff --git a/tests/Images/Input/Bmp/pal8v5.bmp b/tests/Images/Input/Bmp/pal8v5.bmp new file mode 100644 index 000000000..c54647a31 Binary files /dev/null and b/tests/Images/Input/Bmp/pal8v5.bmp differ diff --git a/tests/Images/Input/Bmp/rgb16-565.bmp b/tests/Images/Input/Bmp/rgb16-565.bmp new file mode 100644 index 000000000..c03a27975 Binary files /dev/null and b/tests/Images/Input/Bmp/rgb16-565.bmp differ diff --git a/tests/Images/Input/Bmp/rgb16-565pal.bmp b/tests/Images/Input/Bmp/rgb16-565pal.bmp new file mode 100644 index 000000000..e7632e344 Binary files /dev/null and b/tests/Images/Input/Bmp/rgb16-565pal.bmp differ diff --git a/tests/Images/Input/Bmp/rgb16bfdef.bmp b/tests/Images/Input/Bmp/rgb16bfdef.bmp new file mode 100644 index 000000000..30fe8bb8d Binary files /dev/null and b/tests/Images/Input/Bmp/rgb16bfdef.bmp differ diff --git a/tests/Images/Input/Bmp/rgb24.bmp b/tests/Images/Input/Bmp/rgb24.bmp new file mode 100644 index 000000000..40f8bb094 Binary files /dev/null and b/tests/Images/Input/Bmp/rgb24.bmp differ diff --git a/tests/Images/Input/Bmp/rgb32bf.bmp b/tests/Images/Input/Bmp/rgb32bf.bmp new file mode 100644 index 000000000..20fa9a132 Binary files /dev/null and b/tests/Images/Input/Bmp/rgb32bf.bmp differ diff --git a/tests/Images/Input/Bmp/rgb32bfdef.bmp b/tests/Images/Input/Bmp/rgb32bfdef.bmp new file mode 100644 index 000000000..d7e64e5a4 Binary files /dev/null and b/tests/Images/Input/Bmp/rgb32bfdef.bmp differ diff --git a/tests/Images/Input/Bmp/rgba32-1010102.bmp b/tests/Images/Input/Bmp/rgba32-1010102.bmp new file mode 100644 index 000000000..1a918cebf Binary files /dev/null and b/tests/Images/Input/Bmp/rgba32-1010102.bmp differ diff --git a/tests/Images/Input/Bmp/rgba32.bmp b/tests/Images/Input/Bmp/rgba32.bmp new file mode 100644 index 000000000..829c7c7e3 Binary files /dev/null and b/tests/Images/Input/Bmp/rgba32.bmp differ diff --git a/tests/Images/Input/Bmp/rgba32h56.bmp b/tests/Images/Input/Bmp/rgba32h56.bmp new file mode 100644 index 000000000..343baa330 Binary files /dev/null and b/tests/Images/Input/Bmp/rgba32h56.bmp differ diff --git a/tests/Images/Input/Jpg/baseline/testorig12.jpg b/tests/Images/Input/Jpg/baseline/testorig12.jpg new file mode 100644 index 000000000..861aff98e Binary files /dev/null and b/tests/Images/Input/Jpg/baseline/testorig12.jpg differ diff --git a/tests/Images/Input/Jpg/issues/Issue797-InvalidImage.jpg b/tests/Images/Input/Jpg/issues/Issue797-InvalidImage.jpg new file mode 100644 index 000000000..560d77d47 Binary files /dev/null and b/tests/Images/Input/Jpg/issues/Issue797-InvalidImage.jpg differ diff --git a/tests/Images/Input/Jpg/issues/Issue798-AccessViolationException.jpg b/tests/Images/Input/Jpg/issues/Issue798-AccessViolationException.jpg new file mode 100644 index 000000000..30f61c863 Binary files /dev/null and b/tests/Images/Input/Jpg/issues/Issue798-AccessViolationException.jpg differ