From 5ee5da8f812c7eb97071b2169beade04dab7f2bd Mon Sep 17 00:00:00 2001 From: Stephan Vedder Date: Thu, 6 Dec 2018 23:40:20 +0100 Subject: [PATCH 01/13] Start work on jpeg 12 bit support --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 10 +++++----- .../Formats/Jpg/JpegDecoderTests.Images.cs | 5 ++++- tests/ImageSharp.Tests/TestImages.cs | 4 +++- tests/Images/Input/Jpg/baseline/testorig12.jpg | Bin 0 -> 12394 bytes 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 tests/Images/Input/Jpg/baseline/testorig12.jpg diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index f6da9cb2e..5bfe88cc6 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The only supported precision /// - public const int SupportedPrecision = 8; + public readonly int[] SupportedPrecisions = { 8, 12 }; /// /// The global configuration @@ -137,7 +137,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. @@ -720,10 +720,10 @@ 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 (!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.Frame = new JpegFrame 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1144a3f7c..46f9459c5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -135,12 +135,14 @@ namespace SixLabors.ImageSharp.Tests public const string Testorig420 = "Jpg/baseline/testorig.jpg"; public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg"; public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.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 }; } diff --git a/tests/Images/Input/Jpg/baseline/testorig12.jpg b/tests/Images/Input/Jpg/baseline/testorig12.jpg new file mode 100644 index 0000000000000000000000000000000000000000..861aff98e2a78131bf546b82fd38b00a4f741dd2 GIT binary patch literal 12394 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 Iua1WP4;iQWivR!s literal 0 HcmV?d00001 From ede120d5aa2a5928a0f4e1b75ce86a86aebe32f5 Mon Sep 17 00:00:00 2001 From: Stephan Vedder Date: Sun, 9 Dec 2018 12:26:17 +0100 Subject: [PATCH 02/13] Work on 12-bit support --- src/ImageSharp/Common/Tuples/Vector4Pair.cs | 16 +++---- .../Jpeg/Components/Block8x8F.Generated.cs | 18 ++++---- .../Jpeg/Components/Block8x8F.Generated.tt | 18 ++++---- .../Formats/Jpeg/Components/Block8x8F.cs | 8 ++-- .../JpegColorConverter.FromCmyk.cs | 11 +++-- .../JpegColorConverter.FromGrayScale.cs | 9 ++-- .../JpegColorConverter.FromRgb.cs | 9 ++-- .../JpegColorConverter.FromYCbCrBasic.cs | 4 +- .../JpegColorConverter.FromYCbCrSimd.cs | 26 ++++++------ .../JpegColorConverter.FromYCbCrSimdAvx2.cs | 4 +- .../JpegColorConverter.FromYccK.cs | 21 ++++++---- .../ColorConverters/JpegColorConverter.cs | 42 ++++++++++++++++--- .../Jpeg/Components/Decoder/IRawJpegData.cs | 5 +++ .../Decoder/JpegBlockPostProcessor.cs | 12 +++++- .../Decoder/JpegComponentPostProcessor.cs | 3 +- .../Decoder/JpegImagePostProcessor.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 7 +++- .../Formats/Jpg/Block8x8FTests.cs | 6 +-- .../Formats/Jpg/JpegColorConverterTests.cs | 20 ++++----- 19 files changed, 151 insertions(+), 90 deletions(-) diff --git a/src/ImageSharp/Common/Tuples/Vector4Pair.cs b/src/ImageSharp/Common/Tuples/Vector4Pair.cs index cae283d62..2a42160d6 100644 --- a/src/ImageSharp/Common/Tuples/Vector4Pair.cs +++ b/src/ImageSharp/Common/Tuples/Vector4Pair.cs @@ -37,12 +37,12 @@ namespace SixLabors.ImageSharp.Tuples this.B += other.B; } - /// - /// Downscale method, specific to Jpeg color conversion. Works only if Vector{float}.Count == 4! + /// . Works only if Ve + /// Downscale method, specific to Jpeg color conversctor{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 +50,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 +61,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/Jpeg/Components/Block8x8F.Generated.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs index 09ed6408d..15fbb83d9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs @@ -9,10 +9,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 +90,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(maximum/2 + 1); + 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 +120,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(maximum/2 +1); + 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..431ba86b0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt @@ -22,10 +22,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 +55,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(maximum/2 + 1); + <# PushIndent(" "); @@ -83,10 +83,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(maximum/2 +1); + 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..28f000019 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,17 @@ 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..36bd6fca9 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,17 @@ 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..13eec7aad 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,10 @@ 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..2cab95ff0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs @@ -10,8 +10,8 @@ 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) { } 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..104795710 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,7 +25,7 @@ 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)); @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// /// 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..469695995 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) { } 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..d4e151015 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,21 @@ 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..30dbdc896 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs @@ -22,15 +22,29 @@ 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 +52,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 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 = Converters.FirstOrDefault(c => c.ColorSpace == colorSpace && + c.Precision == precision); if (converter is null) { @@ -63,8 +93,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/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs index 1454bb5b1..83d65c042 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs @@ -29,6 +29,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// JpegColorSpace ColorSpace { get; } + /// + /// Gets the number of bits used for precision. + /// + int Precision { get; } + /// /// Gets the components. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs index da4b2847b..b034198cf 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; @@ -65,7 +72,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The destination buffer area. 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 +86,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..66c9245a3 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..6b5995515 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder MemoryAllocator memoryAllocator = configuration.MemoryAllocator; this.ComponentProcessors = rawJpeg.Components.Select(c => new JpegComponentPostProcessor(memoryAllocator, this, c)).ToArray(); this.rgbaBuffer = memoryAllocator.Allocate(rawJpeg.ImageSizeInPixels.Width); - this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace); + this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace, rawJpeg.Precision); } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 5bfe88cc6..fb10425bd 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -160,6 +160,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public JpegColorSpace ColorSpace { get; private set; } + /// + public int Precision { get; private set; } + /// /// Gets the components. /// @@ -721,11 +724,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InputStream.Read(this.temp, 0, remaining); // We only support 8-bit and 12-bit precision. - if (!SupportedPrecisions.Contains(this.temp[0])) + if (!this.SupportedPrecisions.Contains(this.temp[0])) { 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, 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, From 5ef3f19c1f9a1e6da748db3c0543b05df93b0956 Mon Sep 17 00:00:00 2001 From: Stephan Vedder Date: Sun, 9 Dec 2018 18:26:23 +0100 Subject: [PATCH 03/13] Add missing Non-SIMD implementation --- .../Formats/Jpeg/Components/Block8x8F.Generated.cs | 5 +++-- .../Formats/Jpeg/Components/Block8x8F.Generated.tt | 5 +++-- .../JpegColorConverter.FromYCbCrBasic.cs | 10 +++++----- .../JpegColorConverter.FromYCbCrSimd.cs | 3 ++- .../JpegColorConverter.FromYCbCrSimdAvx2.cs | 12 +++++++----- .../Codecs/Jpeg/YCbCrColorConversion.cs | 6 +++--- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.cs index 15fbb83d9..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; @@ -96,7 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { Vector4 CMin4 = new Vector4(0F); Vector4 CMax4 = new Vector4(maximum); - Vector4 COff4 = new Vector4(maximum/2 + 1); + 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); @@ -122,7 +123,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components [MethodImpl(InliningOptions.ShortMethod)] public void NormalizeColorsAndRoundInplaceAvx2(float maximum) { - Vector off = new Vector(maximum/2 +1); + Vector off = new Vector((float)Math.Ceiling(maximum/2)); Vector max = new Vector(maximum); ref Vector row0 = ref Unsafe.As>(ref this.V0L); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Generated.tt index 431ba86b0..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; @@ -61,7 +62,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { Vector4 CMin4 = new Vector4(0F); Vector4 CMax4 = new Vector4(maximum); - Vector4 COff4 = new Vector4(maximum/2 + 1); + Vector4 COff4 = new Vector4((float)Math.Ceiling(maximum/2)); <# @@ -85,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components [MethodImpl(InliningOptions.ShortMethod)] public void NormalizeColorsAndRoundInplaceAvx2(float maximum) { - Vector off = new Vector(maximum/2 +1); + Vector off = new Vector((float)Math.Ceiling(maximum/2)); Vector max = new Vector(maximum); <# 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 2cab95ff0..124aac122 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrBasic.cs @@ -17,10 +17,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters 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 104795710..5de6f2ffb 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs @@ -28,7 +28,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters 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); } /// 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 469695995..c34bc17ad 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs @@ -28,16 +28,18 @@ 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 +59,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 +72,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/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( From b17bda5e9adf7f2367bb61a57956729763a0a2ed Mon Sep 17 00:00:00 2001 From: Stephan Vedder Date: Mon, 10 Dec 2018 18:39:34 +0100 Subject: [PATCH 04/13] Fix style issues --- src/ImageSharp/Common/Tuples/Vector4Pair.cs | 5 ++--- .../ColorConverters/JpegColorConverter.FromCmyk.cs | 9 +++++---- .../ColorConverters/JpegColorConverter.FromGrayScale.cs | 9 +++++---- .../ColorConverters/JpegColorConverter.FromRgb.cs | 9 +++++---- .../ColorConverters/JpegColorConverter.FromYCbCrSimd.cs | 3 +-- .../JpegColorConverter.FromYCbCrSimdAvx2.cs | 6 ++---- .../ColorConverters/JpegColorConverter.FromYccK.cs | 9 +++++---- .../Decoder/ColorConverters/JpegColorConverter.cs | 3 ++- .../Jpeg/Components/Decoder/JpegBlockPostProcessor.cs | 4 +++- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 4 ++-- 10 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Common/Tuples/Vector4Pair.cs b/src/ImageSharp/Common/Tuples/Vector4Pair.cs index 2a42160d6..2e5ca3137 100644 --- a/src/ImageSharp/Common/Tuples/Vector4Pair.cs +++ b/src/ImageSharp/Common/Tuples/Vector4Pair.cs @@ -37,9 +37,8 @@ namespace SixLabors.ImageSharp.Tuples this.B += other.B; } - /// . Works only if Ve - /// Downscale method, specific to Jpeg color conversctor{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(float downscaleFactor) 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 28f000019..d4dc31fe0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmyk.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmyk.cs @@ -25,10 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1F); - var scale = new Vector4(1 / this.MaximumValue, - 1 / this.MaximumValue, - 1 / this.MaximumValue, - 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.FromGrayScale.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs index 36bd6fca9..4a5dfa632 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScale.cs @@ -19,10 +19,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters public override void ConvertToRgba(in ComponentValues values, Span result) { - var scale = new Vector4(1 / this.MaximumValue, - 1 / this.MaximumValue, - 1 / this.MaximumValue, - 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 13eec7aad..516dfb39f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgb.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgb.cs @@ -24,10 +24,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1); - var scale = new Vector4(1 / this.MaximumValue, - 1 / this.MaximumValue, - 1 / this.MaximumValue, - 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.FromYCbCrSimd.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs index 5de6f2ffb..10ef02a93 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimd.cs @@ -28,8 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount), this.MaximumValue, this.HalfValue); } - FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder), - this.MaximumValue, this.HalfValue); + FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder), this.MaximumValue, this.HalfValue); } /// 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 c34bc17ad..9953f78c1 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrSimdAvx2.cs @@ -28,12 +28,10 @@ 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), - this.MaximumValue, this.HalfValue); + ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount), this.MaximumValue, this.HalfValue); } - FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder), - this.MaximumValue, this.HalfValue);; + FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder), this.MaximumValue, this.HalfValue); } /// 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 d4e151015..94be11e23 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccK.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccK.cs @@ -25,10 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters var v = new Vector4(0, 0, 0, 1F); - var scale = new Vector4(1 / this.MaximumValue, - 1 / this.MaximumValue, - 1 / this.MaximumValue, - 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.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs index 30dbdc896..adcb49bef 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs @@ -28,6 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters new FromCmyk(8), new FromGrayscale(8), new FromRgb(8), + // 12-bit converters GetYCbCrConverter(12), new FromYccK(12), @@ -63,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters private float MaximumValue { get; } /// - /// Gets the maximum value of a sample + /// Gets the half of the maximum value of a sample /// private float HalfValue { get; } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs index b034198cf..ad94572d9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs @@ -65,11 +65,13 @@ 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, diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index fb10425bd..c52858d4c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The only supported precision /// - public readonly int[] SupportedPrecisions = { 8, 12 }; + private readonly int[] supportedPrecisions = { 8, 12 }; /// /// The global configuration @@ -724,7 +724,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InputStream.Read(this.temp, 0, remaining); // We only support 8-bit and 12-bit precision. - if (!this.SupportedPrecisions.Contains(this.temp[0])) + if (!this.supportedPrecisions.Contains(this.temp[0])) { throw new ImageFormatException("Only 8-Bit and 12-Bit precision supported."); } From 4636fe687f98edca2bb546a51db07171e70564af Mon Sep 17 00:00:00 2001 From: Stephan Vedder Date: Mon, 10 Dec 2018 23:40:26 +0100 Subject: [PATCH 05/13] Fix style issues --- .../Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs | 1 - .../Jpeg/Components/Decoder/JpegComponentPostProcessor.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs index ad94572d9..fe39f4188 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBlockPostProcessor.cs @@ -71,7 +71,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The source block. /// The destination buffer area. /// The maximum value derived from the bitdepth. - public void ProcessBlockColorsInto( ref Block8x8 sourceBlock, in BufferArea destArea, diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index 66c9245a3..e7f3e4fda 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -78,7 +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; + float maximumValue = (float)Math.Pow(2, this.ImagePostProcessor.RawJpeg.Precision) - 1; for (int y = 0; y < this.BlockRowsPerStep; y++) { From 133c85489daa305a0c5db9662e070329d8c60989 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 21 Dec 2018 17:57:24 +1100 Subject: [PATCH 06/13] Minor optimizations --- .../ColorConverters/JpegColorConverter.cs | 6 ++---- .../Jpeg/Components/Decoder/IRawJpegData.cs | 2 +- .../Decoder/JpegImagePostProcessor.cs | 17 +++++++++++++---- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 3 +-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs index adcb49bef..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 { @@ -73,8 +71,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// public static JpegColorConverter GetConverter(JpegColorSpace colorSpace, float precision) { - JpegColorConverter converter = Converters.FirstOrDefault(c => c.ColorSpace == colorSpace && - c.Precision == precision); + JpegColorConverter converter = Array.Find(Converters, c => c.ColorSpace == colorSpace + && c.Precision == precision); if (converter is null) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs index 83d65c042..ace8d7215 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// 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/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs index 6b5995515..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,12 +56,18 @@ 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, 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 af6dfd9ec..02ae022e1 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; @@ -169,7 +168,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public JpegComponent[] Components => this.Frame.Components; /// - IEnumerable IRawJpegData.Components => this.Components; + IJpegComponent[] IRawJpegData.Components => this.Components; /// public Block8x8F[] QuantizationTables { get; private set; } From 537dbcf78bf83ef62c5be11fe8bd2db1c598867d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 21 Dec 2018 18:13:19 +1100 Subject: [PATCH 07/13] Update reference images and decoder --- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- tests/Images/External | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/Images/External b/tests/Images/External index 69603ee5b..7ada45bc3 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 69603ee5b6f7dd64114fc44d321e50d9b2d439be +Subproject commit 7ada45bc3484f40e28a50817386ca93f293acd11 From 575c23d929ff81b41d89ef3499125c1899ec2e72 Mon Sep 17 00:00:00 2001 From: Peter Amrehn Date: Fri, 4 Jan 2019 21:37:24 +0100 Subject: [PATCH 08/13] #797 throw ImageFormatException when no StartOfFrame marker is found on a jpg image. --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 5 +++++ .../Formats/Jpg/JpegDecoderTests.Baseline.cs | 8 ++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Jpg/issues/Issue797-InvalidImage.jpg | Bin 0 -> 381 bytes 4 files changed, 14 insertions(+) create mode 100644 tests/Images/Input/Jpg/issues/Issue797-InvalidImage.jpg diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 02ae022e1..936fc0631 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -292,6 +292,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.SOS: if (!metadataOnly) { + if (this.Frame is null) + { + throw new ImageFormatException("no readable SOF marker found."); + } + this.ProcessStartOfScanMarker(); break; } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index 73167a4b7..a2f57e485 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -42,5 +42,13 @@ 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()); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6141e9e2a..460aceba0 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -167,6 +167,7 @@ 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 static readonly string[] All = Baseline.All.Concat(Progressive.All).ToArray(); 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 0000000000000000000000000000000000000000..560d77d47c05984d9ae0d66363a9a3e79e3373ad GIT binary patch literal 381 zcmex=ez+{VquD8S9pEh;o1zT!kvMP6RT*Y!Im zp7>8302;y=8bv_u0R}-11_cHMW=16jCP7AKLB{__7{nPEm>C&R03#DKP(oFJk%5VU zm6?SZCJE#V3Mm?HbS#?0oS1Y`$tV!0ObqA~bp0Uh;)3WBj0}Q`|8Fty0Ig#ZWENzw zXSn_?G`CN4>Eu-#gDGyY6h9Jt=fiuimR&;j&k|?GIo1n54W_dVT2R+L!l=dHyr-2On+Q SzLjaZz2dbUF6{aLZvp^^Po>rX literal 0 HcmV?d00001 From 59d76a0f7b465c27330af6717df479146be3dda9 Mon Sep 17 00:00:00 2001 From: Peter Amrehn Date: Sat, 5 Jan 2019 00:45:43 +0100 Subject: [PATCH 09/13] #797 - apply review remarks --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 936fc0631..06c844d58 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -292,11 +292,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.SOS: if (!metadataOnly) { - if (this.Frame is null) - { - throw new ImageFormatException("no readable SOF marker found."); - } - this.ProcessStartOfScanMarker(); break; } @@ -864,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++) { From 93bb37394a484c03ba84d59d60e39f90677240c7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 9 Jan 2019 01:16:37 +1100 Subject: [PATCH 10/13] Use bounds checks in Huffman ctr. Fix #798 --- .../Jpeg/Components/Decoder/HuffmanTable.cs | 23 ++++++++++-------- .../Formats/Jpg/JpegDecoderTests.Baseline.cs | 10 +++++--- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Issue798-AccessViolationException.jpg | Bin 0 -> 381 bytes 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 tests/Images/Input/Jpg/issues/Issue798-AccessViolationException.jpg 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/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index a2f57e485..1c9d207cd 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -46,9 +46,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [WithFile(TestImages.Jpeg.Issues.InvalidJpegThrowsWrongException797, PixelTypes.Rgba32)] public void LoadingImage_InvalidTagLength_ShouldThrow(TestImageProvider provider) - where TPixel : struct, IPixel - { - Assert.Throws(() => provider.GetImage()); - } + 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 460aceba0..8d8a32fba 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -168,6 +168,7 @@ namespace SixLabors.ImageSharp.Tests 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(); 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 0000000000000000000000000000000000000000..30f61c86305f4533b01e93287ff68ca4f96aa62a GIT binary patch literal 381 zcmex=ez+{VquD8S9pEh;o1zT!kvMP6RT*Y!Im zp7>8302;y=8WmNMS@F85={0eBFjO625aeJ`U{GLYRAOKfWMmd({C|W&oPmLvkpTrT zGBE=sR0S9rm>5`@S(ssxK)#@mqTxozqDjn&Nf(uj0)fiJfIfxk7gYRzi-8Ad9Frik zAcH-_^>3lMeVR)ruPRBLe(zrX9qr9`^CCVn2G+N1%X+iv_wBR4JEKh9Zrw25^y&0A zrjlS8weov&pBq|l4Nu>7XXETip^JL;UhN8(z1nSm_{zs5<*m}|Lnqh1yjRTgpMgL4 UXxsL!Ow;WZukCPQ&;NfD0NVq=R{#J2 literal 0 HcmV?d00001 From b74e230671356be531125a06e4791761b8d68e10 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 18 Jan 2019 07:58:54 +1100 Subject: [PATCH 11/13] Fix Color Filters (#806) * Use Matrix5x4 and fix Hue Filter * Update test references. Fix #802 * Increase tolerance to handle xplat variance. * Rename to ColorMatrix --- src/ImageSharp/Common/Helpers/Vector4Utils.cs | 37 ++ src/ImageSharp/Primitives/ColorMatrix.cs | 459 ++++++++++++++++++ src/ImageSharp/Processing/FilterExtensions.cs | 6 +- .../Processing/KnownFilterMatrices.cs | 358 +++++++------- .../Processors/Filters/FilterProcessor.cs | 36 +- .../Formats/GeneralFormatTests.cs | 3 +- .../Helpers/Vector4UtilsTests.cs | 6 +- .../Primitives/ColorMatrixTests.cs | 270 +++++++++++ .../Processors/Filters/BrightnessTest.cs | 8 +- .../Processors/Filters/ColorBlindnessTest.cs | 9 +- .../Processors/Filters/FilterTest.cs | 20 +- .../TestUtilities/ApproximateFloatComparer.cs | 28 +- tests/Images/External | 2 +- 13 files changed, 1017 insertions(+), 225 deletions(-) create mode 100644 src/ImageSharp/Primitives/ColorMatrix.cs create mode 100644 tests/ImageSharp.Tests/Primitives/ColorMatrixTests.cs 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/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.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/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/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/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/External b/tests/Images/External index 7ada45bc3..74995302e 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 7ada45bc3484f40e28a50817386ca93f293acd11 +Subproject commit 74995302e3c9a5913f1cf09d71b15f888d0ec022 From 1996831667dd1190e6fa82ef416b06db899623b0 Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Fri, 18 Jan 2019 08:11:55 +0100 Subject: [PATCH 12/13] Decoding Bitmaps with BITFIELDS masks (#796) * decoding bitmaps with Bitfields masks * added testcases for Bitfields bitmaps * added parsing of the complete bitmap V4 header to get the color masks infos for the BITFIELDS compression * writing now explicitly a Bitamp v3 header (40 bytes) * added test image for issue #735 * fixed rescaling 5-bit / 6-bit to 0 - 255 range * BitmapEncoder now writes BMP v4 header * using MemoryMarshal.Cast to simplify parsing v4 header * added testcases for bitmap v3, v4, v5 * Bitmap encoder writes again V3 header instead of V4. Additional fields for V4 are zero anyway. * added parsing of special case for 56 bytes headers * using the alpha mask in ReadRgb32BitFields() when its present * added support for bitmasks greater then 8 bits per channel * using MagickReferenceDecoder reference decoder for the test with 10 bits pixel masks * changed bitmap constants to hex * added enum for the bitmap info header type * added support for 52 bytes header (same as 56 bytes without the alpha mask) * clarified comment with difference between imagesharp and magick decoder for Rgba321010102 * BmpEncoder now writes a V4 info header and uses BITFIELDS compression * workaround for issue that the decoder does not decode the alpha channel correctly: For V3 bitmaps, the alpha channel will be ignored during encoding * Fix #732 --- src/ImageSharp/Formats/Bmp/BmpCompression.cs | 2 - src/ImageSharp/Formats/Bmp/BmpConstants.cs | 36 ++ src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 2 - src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 399 ++++++++++++++++-- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 50 ++- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 252 ++++++++++- .../Formats/Bmp/BmpInfoHeaderType.cs | 48 +++ src/ImageSharp/Formats/Bmp/BmpMetaData.cs | 11 +- .../Formats/Bmp/BmpDecoderTests.cs | 107 ++++- tests/ImageSharp.Tests/TestImages.cs | 29 +- tests/Images/Input/Bmp/issue735.bmp | Bin 0 -> 1334298 bytes tests/Images/Input/Bmp/pal8v4.bmp | Bin 0 -> 9322 bytes tests/Images/Input/Bmp/pal8v5.bmp | Bin 0 -> 9338 bytes tests/Images/Input/Bmp/rgb16-565.bmp | Bin 0 -> 16450 bytes tests/Images/Input/Bmp/rgb16-565pal.bmp | Bin 0 -> 17474 bytes tests/Images/Input/Bmp/rgb16bfdef.bmp | Bin 0 -> 16450 bytes tests/Images/Input/Bmp/rgb24.bmp | Bin 0 -> 24630 bytes tests/Images/Input/Bmp/rgb32bf.bmp | Bin 0 -> 32578 bytes tests/Images/Input/Bmp/rgb32bfdef.bmp | Bin 0 -> 32578 bytes tests/Images/Input/Bmp/rgba32-1010102.bmp | Bin 0 -> 32650 bytes tests/Images/Input/Bmp/rgba32.bmp | Bin 0 -> 32650 bytes tests/Images/Input/Bmp/rgba32h56.bmp | Bin 0 -> 32582 bytes 22 files changed, 859 insertions(+), 77 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs create mode 100644 tests/Images/Input/Bmp/issue735.bmp create mode 100644 tests/Images/Input/Bmp/pal8v4.bmp create mode 100644 tests/Images/Input/Bmp/pal8v5.bmp create mode 100644 tests/Images/Input/Bmp/rgb16-565.bmp create mode 100644 tests/Images/Input/Bmp/rgb16-565pal.bmp create mode 100644 tests/Images/Input/Bmp/rgb16bfdef.bmp create mode 100644 tests/Images/Input/Bmp/rgb24.bmp create mode 100644 tests/Images/Input/Bmp/rgb32bf.bmp create mode 100644 tests/Images/Input/Bmp/rgb32bfdef.bmp create mode 100644 tests/Images/Input/Bmp/rgba32-1010102.bmp create mode 100644 tests/Images/Input/Bmp/rgba32.bmp create mode 100644 tests/Images/Input/Bmp/rgba32h56.bmp 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..362fe6443 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) { @@ -146,6 +160,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.ReadRle8(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,12 +219,39 @@ 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. @@ -240,12 +287,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Produce uncompressed bitmap data from RLE8 stream + /// Produce uncompressed bitmap data from 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, 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 +429,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 +468,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 +483,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 +528,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 +556,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 +806,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 +880,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/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 251475567..60e45ae35 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,6 +22,8 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] AllBmpFiles = All; + public static readonly string[] BitfieldsBmpFiles = BitFields; + public static readonly TheoryData RatioFiles = new TheoryData { @@ -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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8d8a32fba..5ecc26657 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -205,12 +205,36 @@ 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 Rgb16565 = "Bmp/rgb16-565.bmp"; + public const string Rgb16bfdef = "Bmp/rgb16bfdef.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, @@ -225,7 +249,8 @@ namespace SixLabors.ImageSharp.Tests Bit8, Bit8Inverted, Bit16, - Bit16Inverted + Bit16Inverted, + Bit32Rgb }; } diff --git a/tests/Images/Input/Bmp/issue735.bmp b/tests/Images/Input/Bmp/issue735.bmp new file mode 100644 index 0000000000000000000000000000000000000000..31fadfcf5ad3513de36517603b39c2ce618d9fe2 GIT binary patch literal 1334298 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

B^TrIxe7B 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 Z8GA6-z_oBqq6aMNTT>=42(&8%{vWj_xZ3~# literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/pal8v4.bmp b/tests/Images/Input/Bmp/pal8v4.bmp new file mode 100644 index 0000000000000000000000000000000000000000..34ebb8030c5f579b9f2a5fdcf95e506fa9e65cf3 GIT binary patch literal 9322 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 RsyqOayU{so7lFE0Q9 literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/pal8v5.bmp b/tests/Images/Input/Bmp/pal8v5.bmp new file mode 100644 index 0000000000000000000000000000000000000000..c54647a31a7c336ceb253275a24d65c737e65304 GIT binary patch literal 9338 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 literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/rgb16-565pal.bmp b/tests/Images/Input/Bmp/rgb16-565pal.bmp new file mode 100644 index 0000000000000000000000000000000000000000..e7632e344b26db844253fff783bca63a6fd944fb GIT binary patch literal 17474 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

3ti*QE>6#EL7i&iwR zY+Kd2`nv(}fyTqD5C%ve^LWcD8v{ol*LbfgUjw2KDL$Yo#D*FinZx{?V3Qo2mXrCJ zW5WwB&JlhV*@%NBIkM0F>i=u{f$*Gv zw%yzJ@1T(xJ5L5A?z$X!V>f$GUXWt`dxasQea)e5`#Zx1)c>pclgj!(!#<-t<359w z;7VvE{1Rv>zLZ{CR0=C2lrhQ#WvJ)m=d9<_=jigla(20_93u=C<_f!nwiTfjc@^@C z1L_C0D0+tssV0XlXjVt;!kr@A=u3~Tirjc?8)NtJ{ZX`NM$F0BMCRoaH%_uo<;C&h zi%$#Bh!UF5wk38ZeIE^cp{L>1h|x%2^V62qHlrPVT~B*e`;I31l1>Lyhs>t#jm%|! zNtm6yH!U~w3uiWOZ*i{hi)glZuOwIYMb1*(o#@FerfP08Rg=k74V%dx0RK7l^I8}5 zE*f4kNw&Cbb;T~lDb?-j(rc@(Z%o^EWB1Md>9h<6>*TG(+m|zMu-RF8*}NRZ{O^eF zHgnqYI`aq2uWG;5&wjg~JyT)-%U_ADq*QV%Ayv34S{1(vT8*!!R~J>oY6vxq8bJ-} z1^ET*h4clwHn5gmE33tbfoC7k^RA|7-KNw(p(t0rCH?zT01qt*6v;>me_3FKI9NFQENaqF#|-v0h1Ep_>Am*iEt~OmlE^ZgW?&tvFPiCzgwWhMM*u{lP{M(;?H< ztu^eS&YJE+mknDD+cbQ;)*kIJ_z|7sBTnfijl44Ire2nQK7wx`7+o=@Zmf8m#874= z9}8AM6W}ipW05P&6D(iYjCEY$n&9=qcPw!QDIwrRh%I$rt*iZ>)jT9Ix4WeKWz zo}ilY1l0%=`bObz!M0FZxGj)YTq~`W-wJ(=e@%Z~^cwbt@P_e5@CNmk{Fe1r`WF2z z@E!Y|>>Z{pxGlG>tIbvtD#??`B@P-`ZAbliM)OT^)19oH?OmMl?yk!gtX{Z@u-$Es zdzi-&&*NUF79}mdvgGE{tY!J$d>?^t#qzop;+2wBveoi&U?ub{ycRJIxzhZsWv$IP z$Ca*Uy=r~O5m%DV2GoYkrG`dwnBNlSCWoeRGQV-=@H&Fsn{gul@m8lW}V89VJ$_XNA-{b_lb-#K=? z{l$60?;<<#eo3C}yIiWcH{FvD399*+pqkEv{*fu`|A+o_8t1hy=wCFtWSTtvvh@}F z6z5d;tIMvfzP>4K`;9#}!_tpr9A}-nm2~?`=1q21R(>`=N03`_r|zzpqnLmGfcaJJ zANtup{=?pxAX7x)|AhTS`NaJM`HcHa`^^6g{eu5O|5Ee?)RyOzB_dg!+z70OCc*0vM#$CXNtSgs zMvkjplf3GDjfksBNda{s_SCS*e5O3XJ~=EcKU2=J=Y?yr})#{B~AKp<>SrHELr|1 a{%66Limtk^;%}1giun(O|9ACD|NjBiCyNsR literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/rgb16bfdef.bmp b/tests/Images/Input/Bmp/rgb16bfdef.bmp new file mode 100644 index 0000000000000000000000000000000000000000..30fe8bb8d6c920dda30b65f074ed9ef85945a610 GIT binary patch literal 16450 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"mu_?!W|wJoS$3E0bUAL9>vh!xeBTvde0)4S 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

1=bCn| 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 literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/rgba32-1010102.bmp b/tests/Images/Input/Bmp/rgba32-1010102.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1a918cebf5caf486faafaf3488ef4be712b09aab GIT binary patch literal 32650 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 INi+2S0m0w)Jpcdz literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/rgba32.bmp b/tests/Images/Input/Bmp/rgba32.bmp new file mode 100644 index 0000000000000000000000000000000000000000..829c7c7e345cb6543affe69ce00f17b16a71ab31 GIT binary patch literal 32650 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 literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Bmp/rgba32h56.bmp b/tests/Images/Input/Bmp/rgba32h56.bmp new file mode 100644 index 0000000000000000000000000000000000000000..343baa3300f331aaeabeb40d8fcd2f232393e815 GIT binary patch literal 32582 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 Date: Sun, 20 Jan 2019 07:54:47 +0100 Subject: [PATCH 13/13] Added support for RLE4 encoded bitmaps (#812) --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 132 +++++++++++++++++- .../Formats/Bmp/BmpDecoderTests.cs | 2 +- .../Formats/Bmp/BmpEncoderTests.cs | 2 +- tests/ImageSharp.Tests/TestImages.cs | 10 +- tests/Images/Input/Bmp/pal4rle.bmp | Bin 0 -> 3836 bytes 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 tests/Images/Input/Bmp/pal4rle.bmp diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 362fe6443..8ca698b87 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -157,7 +157,8 @@ 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; @@ -254,22 +255,31 @@ namespace SixLabors.ImageSharp.Formats.Bmp } ///

- /// 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++) { @@ -287,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. + ///
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. diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 60e45ae35..0ebfbf311 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -29,7 +29,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 } }; [Theory] 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5ecc26657..6e6c7ce47 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -195,7 +195,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"; @@ -209,6 +210,7 @@ namespace SixLabors.ImageSharp.Tests // 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"; @@ -218,8 +220,8 @@ namespace SixLabors.ImageSharp.Tests // Bitmap images with compression type BITFIELDS public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp"; public const string Rgb32bf = "Bmp/rgb32bf.bmp"; - public const string Rgb16565 = "Bmp/rgb16-565.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"; @@ -241,7 +243,9 @@ namespace SixLabors.ImageSharp.Tests F, NegHeight, CoreHeader, - V5Header, RLE, + V5Header, + RLE4, + RLE8, RLEInverted, Bit1, Bit1Pal1, diff --git a/tests/Images/Input/Bmp/pal4rle.bmp b/tests/Images/Input/Bmp/pal4rle.bmp new file mode 100644 index 0000000000000000000000000000000000000000..a5672aebd6247351172b0f602197611554de8e1f GIT binary patch literal 3836 zcmb`J(NE*p6~@2V_&Sbn3u>v1^8d9M89K`5N3ASRxOtsJJ-JFoZtD*y*Jx`{#Oy{ zUyq-4_V3xZ*mGEb#_I{6m+h=SCV^N!PG9RA=}4?nNrDk%iD$7r4`q7HBV{MVR({Ia>8~~-$H5udLK)s(8_qz zec;W{5of%#T^M*7u>%z7b;c~uTHaICB~pj<90$f4HskU6xho^m`qEC14$?Q8Zrn6# z_$J=%c0+fr1)-a9K-HP0D@Z2zgotKMTpp~h6%UIYEc8z^&F{5UdzL280u@S!9)$m$y2{hE zhn{0iq5MeOylAUjE)j~y(q(3N~tI>JZEc4hr{$!oL^0WatUX-YvR9rNO#}h1rwWEijNjbvP(*p#&(?gf}T&wvt zzq{vI((2Pj6SA{R*X9bPaG3(Wua1w7u?Vh2V8PopyFvj2rBp2|s}{AUVJXomM;IdE zB#Y`PuFS;*ZquId-5PAJi!ch5rPZ^Jh59}3bbV5g1WU{{jabJ=;GV$&&$TCd<#(>p zeIc|9o<#6!N*oI@U3>w2n@qSU6J*G8!~~vB!tlusXjwu zRmYiMP5jnsP42Ve(Is<}8;ztmKC>rud%odqem^Atq=w;>E8 zdUq1m4hL#3cj8Rz64PVoD6_gEqBGgd^{<%Sh?62Jejs~r#$Cc!joGSvCVMUtISQ#_ zBxXlpA$?agUBWZ!snYWx<$GBwv5K3a8`IB3Lg5J(WnKsw(4Fk0OH3G-1XRR*Anmze zrYBEJfzUv^MNSwYAsIswt~3yq>6ad7m-0>MQJD9!kjA(`vkfXrCpH-~rhxaPHCS2r z@}7*RXI{8C{tdZ2&Y1lR7t=*s^uI*@tWiAfHKk^Wbj>JKKK{rlKYCTq7yTQY#mfo1 zJ+bn`&bSKh zQ$D9?F%r?_;J}_F5L6U=e^RE3b8Wvnrdl z{VpGAb=ofe6m=}r%rvv}o0K?#KRQu_&4@X!I0Uny#Z)ymB1Z6v0c0Um>OTF*DlaAz zEzP+a@fV52kXGzerIM(_tj{Z~Ndej*?*z)%P7DP*ut_ZAQV&A=a^^~CrOJxj_FPyI zj1iQgD>^gkZd!p~@!375Tsi-dzAG?%u}tQr52WP&KwY`7{<`7MHBPg0def+|m|HQP zOV#jri(rmy#cU0pqm-?YPqZxh+K*Nhdc3tX<>BcYg&g1enlxq(8)&v#Uet<)@$Kz+ z+sj()#iMw1!&6q_C4#}giw03UzPj2CsoY9LnETGy9+={gm7#w;%WuPsx8tp%kClvz zzdP)P)Qz{lKSw5%_jIPv#n&)ym#apsKG;eR%({$Sz_|LtgPkVQHR=zV3EyJXiAAe? zAnR1fjQiS<`cP4hYZ9)Lb8=<9U1?V~8Xx@R<8l=$zKLPn;7>i(Q-}FseOP}j&-UxA za?8!4EKzL7FmGV=dVJo~17pbN!|UXV&+SZA zec)Ot^>mH?yslW-^QUV=u_WUtWcbR#A9Cd~;oHRDOg?t1S=*`Z_=%r%dz}}5L1(yo^J|3n z+`ehw68j_84DCZ*CYQXk=))2pBi2`f)kkPvL*Tx5O6#S!&9}{W(M5a_C%2vL&i19W zSXy$^JNV(?BsdA)N7wOnoLuqrnQu(9P484!y=z~WDbICg)q^j!u@S5NF%cMa{ zJz^zmk>W3vi2H)!z|qSu%GSo_CePFh=>Ej$z^Zn3I{bdA`D!N}>%qw%es^fSe)ERo z*E|9ezhU~c6icUF*1P0^krdvwvrIT06d21a7mTLxe)yBwsHfQ|JI=cEcKz-8MOfJ6 kubxT1!yr1BTg4r$B>(Ik_P%2fe|wF>8+b|b*^18n8}acBDF6Tf literal 0 HcmV?d00001