// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; [Trait("Category", "PixelFormats")] public class PixelBlenderTests { public static TheoryData BlenderMappings = new() { { new TestPixel(), typeof(DefaultPixelBlenders.NormalSrcOver), PixelColorBlendingMode.Normal }, { new TestPixel(), typeof(DefaultPixelBlenders.ScreenSrcOver), PixelColorBlendingMode.Screen }, { new TestPixel(), typeof(DefaultPixelBlenders.HardLightSrcOver), PixelColorBlendingMode.HardLight }, { new TestPixel(), typeof(DefaultPixelBlenders.OverlaySrcOver), PixelColorBlendingMode.Overlay }, { new TestPixel(), typeof(DefaultPixelBlenders.DarkenSrcOver), PixelColorBlendingMode.Darken }, { new TestPixel(), typeof(DefaultPixelBlenders.LightenSrcOver), PixelColorBlendingMode.Lighten }, { new TestPixel(), typeof(DefaultPixelBlenders.AddSrcOver), PixelColorBlendingMode.Add }, { new TestPixel(), typeof(DefaultPixelBlenders.SubtractSrcOver), PixelColorBlendingMode.Subtract }, { new TestPixel(), typeof(DefaultPixelBlenders.MultiplySrcOver), PixelColorBlendingMode.Multiply }, { new TestPixel(), typeof(DefaultPixelBlenders.NormalSrcOver), PixelColorBlendingMode.Normal }, { new TestPixel(), typeof(DefaultPixelBlenders.ScreenSrcOver), PixelColorBlendingMode.Screen }, { new TestPixel(), typeof(DefaultPixelBlenders.HardLightSrcOver), PixelColorBlendingMode.HardLight }, { new TestPixel(), typeof(DefaultPixelBlenders.OverlaySrcOver), PixelColorBlendingMode.Overlay }, { new TestPixel(), typeof(DefaultPixelBlenders.DarkenSrcOver), PixelColorBlendingMode.Darken }, { new TestPixel(), typeof(DefaultPixelBlenders.LightenSrcOver), PixelColorBlendingMode.Lighten }, { new TestPixel(), typeof(DefaultPixelBlenders.AddSrcOver), PixelColorBlendingMode.Add }, { new TestPixel(), typeof(DefaultPixelBlenders.SubtractSrcOver), PixelColorBlendingMode.Subtract }, { new TestPixel(), typeof(DefaultPixelBlenders.MultiplySrcOver), PixelColorBlendingMode.Multiply }, }; public static TheoryData BlenderModeMappings { get { TheoryData data = new(); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Clear, typeof(DefaultPixelBlenders.NormalClear), typeof(DefaultPixelBlenders.MultiplyClear), typeof(DefaultPixelBlenders.AddClear), typeof(DefaultPixelBlenders.SubtractClear), typeof(DefaultPixelBlenders.ScreenClear), typeof(DefaultPixelBlenders.DarkenClear), typeof(DefaultPixelBlenders.LightenClear), typeof(DefaultPixelBlenders.OverlayClear), typeof(DefaultPixelBlenders.HardLightClear)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Xor, typeof(DefaultPixelBlenders.NormalXor), typeof(DefaultPixelBlenders.MultiplyXor), typeof(DefaultPixelBlenders.AddXor), typeof(DefaultPixelBlenders.SubtractXor), typeof(DefaultPixelBlenders.ScreenXor), typeof(DefaultPixelBlenders.DarkenXor), typeof(DefaultPixelBlenders.LightenXor), typeof(DefaultPixelBlenders.OverlayXor), typeof(DefaultPixelBlenders.HardLightXor)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Src, typeof(DefaultPixelBlenders.NormalSrc), typeof(DefaultPixelBlenders.MultiplySrc), typeof(DefaultPixelBlenders.AddSrc), typeof(DefaultPixelBlenders.SubtractSrc), typeof(DefaultPixelBlenders.ScreenSrc), typeof(DefaultPixelBlenders.DarkenSrc), typeof(DefaultPixelBlenders.LightenSrc), typeof(DefaultPixelBlenders.OverlaySrc), typeof(DefaultPixelBlenders.HardLightSrc)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcAtop, typeof(DefaultPixelBlenders.NormalSrcAtop), typeof(DefaultPixelBlenders.MultiplySrcAtop), typeof(DefaultPixelBlenders.AddSrcAtop), typeof(DefaultPixelBlenders.SubtractSrcAtop), typeof(DefaultPixelBlenders.ScreenSrcAtop), typeof(DefaultPixelBlenders.DarkenSrcAtop), typeof(DefaultPixelBlenders.LightenSrcAtop), typeof(DefaultPixelBlenders.OverlaySrcAtop), typeof(DefaultPixelBlenders.HardLightSrcAtop)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcIn, typeof(DefaultPixelBlenders.NormalSrcIn), typeof(DefaultPixelBlenders.MultiplySrcIn), typeof(DefaultPixelBlenders.AddSrcIn), typeof(DefaultPixelBlenders.SubtractSrcIn), typeof(DefaultPixelBlenders.ScreenSrcIn), typeof(DefaultPixelBlenders.DarkenSrcIn), typeof(DefaultPixelBlenders.LightenSrcIn), typeof(DefaultPixelBlenders.OverlaySrcIn), typeof(DefaultPixelBlenders.HardLightSrcIn)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcOut, typeof(DefaultPixelBlenders.NormalSrcOut), typeof(DefaultPixelBlenders.MultiplySrcOut), typeof(DefaultPixelBlenders.AddSrcOut), typeof(DefaultPixelBlenders.SubtractSrcOut), typeof(DefaultPixelBlenders.ScreenSrcOut), typeof(DefaultPixelBlenders.DarkenSrcOut), typeof(DefaultPixelBlenders.LightenSrcOut), typeof(DefaultPixelBlenders.OverlaySrcOut), typeof(DefaultPixelBlenders.HardLightSrcOut)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Dest, typeof(DefaultPixelBlenders.NormalDest), typeof(DefaultPixelBlenders.MultiplyDest), typeof(DefaultPixelBlenders.AddDest), typeof(DefaultPixelBlenders.SubtractDest), typeof(DefaultPixelBlenders.ScreenDest), typeof(DefaultPixelBlenders.DarkenDest), typeof(DefaultPixelBlenders.LightenDest), typeof(DefaultPixelBlenders.OverlayDest), typeof(DefaultPixelBlenders.HardLightDest)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestAtop, typeof(DefaultPixelBlenders.NormalDestAtop), typeof(DefaultPixelBlenders.MultiplyDestAtop), typeof(DefaultPixelBlenders.AddDestAtop), typeof(DefaultPixelBlenders.SubtractDestAtop), typeof(DefaultPixelBlenders.ScreenDestAtop), typeof(DefaultPixelBlenders.DarkenDestAtop), typeof(DefaultPixelBlenders.LightenDestAtop), typeof(DefaultPixelBlenders.OverlayDestAtop), typeof(DefaultPixelBlenders.HardLightDestAtop)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestIn, typeof(DefaultPixelBlenders.NormalDestIn), typeof(DefaultPixelBlenders.MultiplyDestIn), typeof(DefaultPixelBlenders.AddDestIn), typeof(DefaultPixelBlenders.SubtractDestIn), typeof(DefaultPixelBlenders.ScreenDestIn), typeof(DefaultPixelBlenders.DarkenDestIn), typeof(DefaultPixelBlenders.LightenDestIn), typeof(DefaultPixelBlenders.OverlayDestIn), typeof(DefaultPixelBlenders.HardLightDestIn)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestOut, typeof(DefaultPixelBlenders.NormalDestOut), typeof(DefaultPixelBlenders.MultiplyDestOut), typeof(DefaultPixelBlenders.AddDestOut), typeof(DefaultPixelBlenders.SubtractDestOut), typeof(DefaultPixelBlenders.ScreenDestOut), typeof(DefaultPixelBlenders.DarkenDestOut), typeof(DefaultPixelBlenders.LightenDestOut), typeof(DefaultPixelBlenders.OverlayDestOut), typeof(DefaultPixelBlenders.HardLightDestOut)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestOver, typeof(DefaultPixelBlenders.NormalDestOver), typeof(DefaultPixelBlenders.MultiplyDestOver), typeof(DefaultPixelBlenders.AddDestOver), typeof(DefaultPixelBlenders.SubtractDestOver), typeof(DefaultPixelBlenders.ScreenDestOver), typeof(DefaultPixelBlenders.DarkenDestOver), typeof(DefaultPixelBlenders.LightenDestOver), typeof(DefaultPixelBlenders.OverlayDestOver), typeof(DefaultPixelBlenders.HardLightDestOver)); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcOver, typeof(DefaultPixelBlenders.NormalSrcOver), typeof(DefaultPixelBlenders.MultiplySrcOver), typeof(DefaultPixelBlenders.AddSrcOver), typeof(DefaultPixelBlenders.SubtractSrcOver), typeof(DefaultPixelBlenders.ScreenSrcOver), typeof(DefaultPixelBlenders.DarkenSrcOver), typeof(DefaultPixelBlenders.LightenSrcOver), typeof(DefaultPixelBlenders.OverlaySrcOver), typeof(DefaultPixelBlenders.HardLightSrcOver)); return data; } } [Theory] [MemberData(nameof(BlenderMappings))] public void ReturnsCorrectBlender(TestPixel pixel, Type type, PixelColorBlendingMode mode) where TPixel : unmanaged, IPixel { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(mode, PixelAlphaCompositionMode.SrcOver); Assert.IsType(type, blender); } [Theory] [MemberData(nameof(BlenderModeMappings))] public void ReturnsCorrectBlenderForAllModeCombinations(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode, Type type) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); Assert.IsType(type, blender); } [Fact] public void BlendFunctionsAreCalledForAllModeCombinations() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ExerciseAllBlenderModeCombinations, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); [Fact] public void Blend_WithConstantSourceAndSingleAmount() { PixelBlender blender = new DefaultPixelBlenders.NormalSrcOver(); Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); blender.Blend(Configuration.Default, destination, background, source, 1F); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithConstantSourceSingleAmountAndWorkingBuffer() { PixelBlender blender = new DefaultPixelBlenders.NormalSrcOver(); Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); Vector4[] workingBuffer = new Vector4[destination.Length * 2]; blender.Blend(Configuration.Default, destination, background, source, 1F, workingBuffer); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithConstantSourceAndAmountSpan() { PixelBlender blender = new DefaultPixelBlenders.NormalSrcOver(); Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); float[] amount = [1F, 1F]; blender.Blend(Configuration.Default, destination, background, source, amount); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithConstantSourceAmountSpanAndWorkingBuffer() { PixelBlender blender = new DefaultPixelBlenders.NormalSrcOver(); Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); float[] amount = [1F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 2]; blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithSourceSpanAmountSpanAndWorkingBuffer() { PixelBlender blender = new DefaultPixelBlenders.NormalSrcOver(); Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32[] source = [ Color.Blue.ToPixel(), Color.Yellow.ToPixel() ]; float[] amount = [1F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 3]; blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer); Assert.Equal(source[0], destination[0]); Assert.Equal(source[1], destination[1]); } public static TheoryData ColorBlendingExpectedResults = new() { { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Normal, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Screen, new Rgba32(0xFFEEE7FF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.HardLight, new Rgba32(0xFFC62D32) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Overlay, new Rgba32(0xFFDDCEFF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Darken, new Rgba32(0xFF701919) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Lighten, new Rgba32(0xFFE1E4FF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Add, new Rgba32(0xFFFFFDFF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Subtract, new Rgba32(0xFF71CBE6) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Multiply, new Rgba32(0xFF631619) }, }; [Theory] [MemberData(nameof(ColorBlendingExpectedResults))] public void TestColorBlendingModes(Rgba32 backdrop, Rgba32 source, float opacity, PixelColorBlendingMode mode, Rgba32 expectedResult) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(mode, PixelAlphaCompositionMode.SrcOver); Rgba32 actualResult = blender.Blend(backdrop, source, opacity); // var str = actualResult.Rgba.ToString("X8"); // used to extract expectedResults Assert.Equal(actualResult.ToVector4(), expectedResult.ToVector4()); } public static TheoryData AlphaCompositionExpectedResults = new() { { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Clear, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Xor, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Dest, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestAtop, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestIn, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestOut, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestOver, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Src, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcAtop, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcIn, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcOut, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcOver, Color.MidnightBlue.ToPixel() }, }; [Theory] [MemberData(nameof(AlphaCompositionExpectedResults))] public void TestAlphaCompositionModes(Rgba32 backdrop, Rgba32 source, float opacity, PixelAlphaCompositionMode mode, Rgba32 expectedResult) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, mode); Rgba32 actualResult = blender.Blend(backdrop, source, opacity); // var str = actualResult.Rgba.ToString("X8"); // used to extract expectedResults Assert.Equal(actualResult.ToVector4(), expectedResult.ToVector4()); } private static void AddBlenderModeMappings( TheoryData data, PixelAlphaCompositionMode alphaMode, Type normal, Type multiply, Type add, Type subtract, Type screen, Type darken, Type lighten, Type overlay, Type hardLight) { data.Add(PixelColorBlendingMode.Normal, alphaMode, normal); data.Add(PixelColorBlendingMode.Multiply, alphaMode, multiply); data.Add(PixelColorBlendingMode.Add, alphaMode, add); data.Add(PixelColorBlendingMode.Subtract, alphaMode, subtract); data.Add(PixelColorBlendingMode.Screen, alphaMode, screen); data.Add(PixelColorBlendingMode.Darken, alphaMode, darken); data.Add(PixelColorBlendingMode.Lighten, alphaMode, lighten); data.Add(PixelColorBlendingMode.Overlay, alphaMode, overlay); data.Add(PixelColorBlendingMode.HardLight, alphaMode, hardLight); } private static void ExerciseAllBlenderModeCombinations() { foreach (PixelAlphaCompositionMode alphaMode in Enum.GetValues()) { foreach (PixelColorBlendingMode colorMode in Enum.GetValues()) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); ExerciseBlender(blender); } } } private static void ExerciseBlender(PixelBlender blender) { Rgba32 background = Color.MistyRose.ToPixel(); Rgba32 source = Color.MidnightBlue.ToPixel(); float[] amount = [1F, 1F, 1F, 1F]; Rgba32 expected = blender.Blend(background, source, 1F); Rgba32[] destination = new Rgba32[4]; Rgba32[] backgroundSpan = [background, background, background, background]; Rgba32[] sourceSpan = [source, source, source, source]; Vector4[] sourceSpanBuffer = new Vector4[destination.Length * 3]; Vector4[] constantSourceBuffer = new Vector4[destination.Length * 2]; blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, 1F, sourceSpanBuffer); Assert.All(destination, x => Assert.Equal(expected, x)); blender.Blend(Configuration.Default, destination, backgroundSpan, source, 1F, constantSourceBuffer); Assert.All(destination, x => Assert.Equal(expected, x)); blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, sourceSpanBuffer); Assert.All(destination, x => Assert.Equal(expected, x)); blender.Blend(Configuration.Default, destination, backgroundSpan, source, amount, constantSourceBuffer); Assert.All(destination, x => Assert.Equal(expected, x)); } }