diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs new file mode 100644 index 0000000000..4b49e32c11 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaBlendingInputLayer +{ + public ReadOnlyMemory R { get; set; } + + public ReadOnlyMemory G { get; set; } + + public ReadOnlyMemory B { get; set; } + + public ReadOnlyMemory A { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs new file mode 100644 index 0000000000..c5db8cb8a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaBlendingOutput +{ + public Memory R { get; set; } + + public Memory G { get; set; } + + public Memory B { get; set; } + + public Memory A { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs new file mode 100644 index 0000000000..641205ab21 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaHelper +{ + // TODO: SIMD support + private const float SmallAlpha = 1f / (1 << 26); + + // Force x to stay within the range of 0 through 1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Clamp(float x) => Math.Clamp(x, 0f, 1f); + + public static void PerformAlphaBlending( + JxlAlphaBlendingInputLayer background, + JxlAlphaBlendingInputLayer foreground, + JxlAlphaBlendingOutput output, + int pixelCount, + bool alphaIsPremultiplied, + bool clamp) + { + // Store all channels from all parameters into spans, because + // creating a Span from Memory is expensive, especially + // in a loop. + ReadOnlySpan fgR = foreground.R.Span; + ReadOnlySpan fgG = foreground.G.Span; + ReadOnlySpan fgB = foreground.B.Span; + ReadOnlySpan fgA = foreground.A.Span; + + ReadOnlySpan bgR = background.R.Span; + ReadOnlySpan bgG = background.G.Span; + ReadOnlySpan bgB = background.B.Span; + ReadOnlySpan bgA = background.A.Span; + + Span outR = output.R.Span; + Span outG = output.G.Span; + Span outB = output.B.Span; + Span outA = output.A.Span; + + if (alphaIsPremultiplied) + { + for (int x = 0; x < pixelCount; x++) + { + float fga = clamp ? Clamp(fgA[x]) : fgA[x]; + outR[x] = fgR[x] + (bgR[x] * (1f - fga)); + outG[x] = fgG[x] + (bgG[x] * (1f - fga)); + outB[x] = fgB[x] + (bgB[x] * (1f - fga)); + outA[x] = 1f - ((1f - fga) * (1f - bgA[x])); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + float fga = clamp ? Clamp(fgA[x]) : fgA[x]; + float newA = 1f - ((1f - fga) * (1f - bgA[x])); + float rnewA = newA > 0 ? 1f / newA : 0f; + outR[x] = ((fgR[x] * fga) + (bgR[x] * bgA[x] * (1f - fga))) * rnewA; + outG[x] = ((fgG[x] * fga) + (bgG[x] * bgA[x] * (1f - fga))) * rnewA; + outB[x] = ((fgB[x] * fga) + (bgB[x] * bgA[x] * (1f - fga))) * rnewA; + outA[x] = newA; + } + } + } + + public static void PerformAlphaBlending( + ReadOnlySpan bg, + ReadOnlySpan bga, + ReadOnlySpan fg, + ReadOnlySpan fga, + Span output, + int pixelCount, + bool alphaIsPremultiplied, + bool clamp) + { + if (bg == bga && fg == fga) + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + output[x] = 1f - ((1f - fa) * (1f - bga[x])); + } + } + else + { + if (alphaIsPremultiplied) + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + output[x] = fg[x] + (bg[x] * (1f - fa)); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + float new_a = 1f - ((1f - fa) * (1f - bga[x])); + float rnew_a = new_a > 0 ? 1f / new_a : 0f; + output[x] = ((fg[x] * fa) + (bg[x] * bga[x] * (1f - fa))) * rnew_a; + } + } + } + } + + public static void PerformAlphaWeightedAdd( + ReadOnlySpan bg, + ReadOnlySpan fg, + ReadOnlySpan fga, + Span output, + int pixelCount, + bool clamp) + { + if (fg == fga) + { + bg[pixelCount..].CopyTo(output); + } + else if (clamp) + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] + (fg[x] * Clamp(fga[x])); + } + } + else + { + for (int x = 0; x < pixelCount; ++x) + { + output[x] = bg[x] + (fg[x] * fga[x]); + } + } + } + + public static void PerformMultiplyBlending( + ReadOnlySpan bg, + ReadOnlySpan fg, + Span output, + int pixelCount, + bool clamp) + { + if (clamp) + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] * Clamp(fg[x]); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] * fg[x]; + } + } + } + + public static void PremultiplyAlpha( + Span r, + Span g, + Span b, + ReadOnlySpan a, + int pixelCount) + { + for (int x = 0; x < pixelCount; x++) + { + float multiplier = Math.Max(SmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } + } + + public static void UnpremultiplyAlpha( + Span r, + Span g, + Span b, + ReadOnlySpan a, + int pixelCount) + { + for (int x = 0; x < pixelCount; x++) + { + float multiplier = 1f / Math.Max(SmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } + } +}