From 0d159b9477c3bc4b563ad5f02774dd38f6e96fda Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:44:39 +0400 Subject: [PATCH] Add CMS* prototype, XYB decoding, TOC, convolution and coefficient ordering * CMS - Color Management System --- .../Formats/Jxl/Cms/JxlCieXyPrimaries.cs | 27 ++ .../Formats/Jxl/Cms/JxlColorEncoding.cs | 77 ++++ .../Formats/Jxl/Cms/JxlColorSpace.cs | 31 ++ .../Jxl/Cms/JxlCustomTransferFunction.cs | 109 +++++ src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs | 53 +++ .../Formats/Jxl/Cms/JxlPrimaries.cs | 27 ++ .../Formats/Jxl/Cms/JxlRenderingIntent.cs | 13 + .../Formats/Jxl/Cms/JxlTransferFunction.cs | 45 ++ .../Formats/Jxl/Cms/JxlWhitePoint.cs | 33 ++ src/ImageSharp/Formats/Jxl/Cms/README.md | 4 + .../Jxl/IO/Metadata/JxlImageMetadata.cs | 22 + .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 8 +- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 + .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 157 +++++++ .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 63 +++ .../Processing/Decoder/JxlOpsinParameters.cs | 18 + .../Decoder/JxlOutputEncodingInfo.cs | 78 ++++ .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 180 ++++++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 91 ++++ .../Formats/Jxl/Processing/JxlConvolve.cs | 424 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDctScales.cs | 2 +- .../Formats/Jxl/Processing/JxlToc.cs | 191 ++++++++ .../Formats/Jxl/Processing/JxlXorShift.cs | 2 + 23 files changed, 1661 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/README.md create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs new file mode 100644 index 000000000..7e5a4367e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// RGB primaries for CIEXY +/// +internal struct JxlCieXyPrimaries +{ + /// + /// Gets or sets the R component + /// + public CieXyChromaticityCoordinates R { get; set; } + + /// + /// Gets or sets the G component + /// + public CieXyChromaticityCoordinates G { get; set; } + + /// + /// Gets or sets the B component + /// + public CieXyChromaticityCoordinates B { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs new file mode 100644 index 000000000..9a87dccc2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal sealed class JxlColorEncoding +{ + public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65; + + public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb; + + public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative; + + public bool HaveFields { get; set; } = true; + + public JxlIccBytes? Icc { get; set; } + + public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb; + + public bool Cmyk { get; set; } + + public JxlCustomTransferFunction TransferFunction { get; set; } + + public JxlCustomXy White { get; set; } + + public JxlCustomXy Red { get; set; } + + public JxlCustomXy Green { get; set; } + + public JxlCustomXy Blue { get; set; } + + public bool HasPrimaries => this.ColorSpace is not (JxlColorSpace.Gray or JxlColorSpace.Xyb); + + public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3; + + public bool TryGetPrimaries(out JxlCieXyPrimaries xy) + { + xy = default; + + if (!this.HasPrimaries || !this.HasPrimaries) + { + return false; + } + + switch (this.Primaries) + { + case JxlPrimaries.Custom: + xy.R = this.Red.GetValue(); + xy.G = this.Green.GetValue(); + xy.B = this.Blue.GetValue(); + break; + + case JxlPrimaries.SRgb: + xy.R = new(0.639998686f, 0.330010138f); + xy.G = new(0.300003784f, 0.600003357f); + xy.B = new(0.150002046f, 0.059997204f); + break; + + case JxlPrimaries.Bt2020: + xy.R = new(0.708f, 0.292f); + xy.G = new(0.170f, 0.797f); + xy.B = new(0.131f, 0.046f); + break; + + case JxlPrimaries.P3: + xy.R = new(0.680f, 0.320f); + xy.G = new(0.265f, 0.690f); + xy.B = new(0.150f, 0.060f); + break; + + default: + throw new InvalidOperationException("Invalid primaries: " + this.Primaries); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs new file mode 100644 index 000000000..b65cbbef9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Supported, JPEG XL-specific color space types. +/// +internal enum JxlColorSpace : byte +{ + /// + /// Trichromatic color data. This also includes CMYK if Black + /// ExtraChannelInfo is present. + /// + Rgb, + + /// + /// Single-channel data. + /// + Gray, + + /// + /// Like Rgb but fixed values for primaries. + /// + Xyb, + + /// + /// Unknown color space + /// + Unknown +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs new file mode 100644 index 000000000..acf049d4a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal struct JxlCustomTransferFunction +{ + private const uint MaxGamma = 8192; + private const uint GammaMultiplier = 10000000; + + public JxlCustomTransferFunction() + { + } + + public bool HaveGamma { get; set; } + + public uint Gamma { get; set; } + + public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb; + + public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown; + + public readonly bool IsSrgb => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.SRgb; + + public readonly bool IsLinear => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Linear; + + public readonly bool IsPq => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Pq; + + public readonly bool IsHlg => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Hlg; + + public readonly bool Is709 => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Bt709; + + public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci; + + public readonly JxlTransferFunction GetTransferFunction() + { + if (this.HaveGamma) + { + return JxlTransferFunction.Unknown; + } + + return this.TransferFunction; + } + + public void SetTransferFunction(JxlTransferFunction tf) + { + this.HaveGamma = false; + this.TransferFunction = tf; + } + + public readonly float GetGamma() + { + if (!this.HaveGamma) + { + return 0.0f; + } + + return this.Gamma * (1.0f / GammaMultiplier); + } + + public void SetGamma(float newGamma) + { + if (newGamma is < 1.0f / MaxGamma or > 1.0f) + { + throw new InvalidOperationException($"Invalid gamma {newGamma}"); + } + + this.HaveGamma = false; + + if (IsAlmostEqual(newGamma, 1.0f)) + { + this.TransferFunction = JxlTransferFunction.Linear; + return; + } + + if (IsAlmostEqual(newGamma, 1.0f / 2.6f)) + { + this.TransferFunction = JxlTransferFunction.Dci; + return; + } + + // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel + // values because those curves also have a linear part. + this.HaveGamma = true; + this.Gamma = (uint)MathF.Round((float)(newGamma * GammaMultiplier)); + this.TransferFunction = JxlTransferFunction.Unknown; + } + + public readonly bool IsSame(JxlCustomTransferFunction other) + { + if (this.HaveGamma != other.HaveGamma) + { + return false; + } + + if (this.HaveGamma) + { + return this.Gamma == other.Gamma; + } + + return this.TransferFunction == other.TransferFunction; + } + + private static bool IsAlmostEqual(float a, float b) + { + const float dist = 1e-3f; + return MathF.Abs(a - b) < dist; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs new file mode 100644 index 000000000..95652cdaf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// A serializable form of CieXyChromaticityCoordinates +/// +internal struct JxlCustomXy +{ + private const uint Multiplier = 1000000; + private const float RoughLimit = 4.0f; + private const int Min = -0x200000; + private const int Max = 0x1FFFFF; + + public int X { get; set; } + + public int Y { get; set; } + + public readonly CieXyChromaticityCoordinates GetValue() => new( + x: this.X * (1.0f / Multiplier), + y: this.Y * (1.0f / Multiplier)); + + public bool SetValue(CieXyChromaticityCoordinates xy) + { + bool ok = (Math.Abs(xy.X) < RoughLimit) && (Math.Abs(xy.Y) < RoughLimit); + + if (!ok) + { + throw new InvalidOperationException("X or Y is out of bounds"); + } + + this.X = (int)MathF.Round((float)(xy.X * Multiplier)); + + if (this.X is < Min or > Max) + { + throw new InvalidOperationException("X is out of bounds"); + } + + this.Y = (int)MathF.Round((float)(xy.Y * Multiplier)); + + if (this.Y is < Min or > Max) + { + throw new InvalidOperationException("Y is out of bounds"); + } + + return true; + } + + public readonly bool IsSame(CieXyChromaticityCoordinates other) => this.X == other.X && this.Y == other.Y; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs new file mode 100644 index 000000000..48d4766fa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// JPEG XL primaries +/// +internal enum JxlPrimaries : byte +{ + /// + /// Same as ITU-R BT.709 + /// + SRgb = 1, + + /// + /// Values encoded in separate fields + /// + Custom = 2, + + /// + /// ITU-R BT.2020 + /// + Bt2020 = 9, + + P3 = 11, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs new file mode 100644 index 000000000..739dddccc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal enum JxlRenderingIntent : byte +{ + // Values match ICC sRGB encodings + Perceptual, // Good for photos, requires a profile with LUT + Relative, // Good for logos + Saturation, // Perhaps useful for CG with fully saturated colors + Absolute, // Leaves white point unchanged; good for proofing +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs new file mode 100644 index 000000000..cfee68176 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// JPEG XL transfer function type +/// +internal enum JxlTransferFunction : byte +{ + /// + /// ITU-R BT.709 + /// + Bt709 = 1, + + /// + /// Unknown transfer function + /// + Unknown = 2, + + /// + /// Linear transfer function + /// + Linear = 8, + + /// + /// sRGB + /// + SRgb = 13, + + /// + /// From ITU-R BT.2100 + /// + Pq = 16, + + /// + /// From SMPTE RP 431-2 reference projector + /// + Dci = 17, + + /// + /// From ITU-R BT.2100 + /// + Hlg = 18, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs new file mode 100644 index 000000000..c6c7e019f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// White point from CICP Color Primaries. +/// +// Note that we define a separate enum instead of using the ColorPrimaries +// enum from CICP code because JPEG XL doesn't support all color primaries defined +// by CICP. +internal enum JxlWhitePoint : byte +{ + /// + /// sRGB/ITU-R BT.709/Display P3/ITU-R BT.2020 + /// + D65 = 1, + + /// + /// Actual values encoded in separate fields + /// + Custom = 2, + + /// + /// XYZ + /// + E = 10, + + /// + /// DCI-P3 + /// + Dci = 11, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/README.md b/src/ImageSharp/Formats/Jxl/Cms/README.md new file mode 100644 index 000000000..557a4c8b9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/README.md @@ -0,0 +1,4 @@ +# CMS +This is the JPEG XL Color Management System component. + +Not to be confused with Content Management System. diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index d9bc419e9..09400f9a8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -124,5 +125,26 @@ internal sealed class JxlImageMetadata : IJxlFields this.Modular16BitBufferSufficient = bits <= 12; } + public void SetIntensityTarget() + { + JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction; + + if (tf is not null) + { + if (tf.Value.IsPq) + { + this.SetIntensityTarget(10000); + } + else if (tf.Value.IsHlg) + { + this.SetIntensityTarget(1000); + } + else + { + this.SetIntensityTarget(DefaultIntensityTarget); + } + } + } + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index 357ce27c3..dc44594d9 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -12,9 +12,13 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields public JxlMatrix3x3F InverseMatrix { get; set; } - public InlineArray3 OpsinBiases { get; set; } + // Prefer arrays so we can set values like this: + // JxlOpsinInverseMatrix m = ...; + // m.OpsinBiases[0] = 1f; + // An InlineArray can't do that. + public float[] OpsinBiases { get; set; } = new float[3]; - public InlineArray4 QuantBiases { get; set; } + public float[] QuantBiases { get; set; } = new float[4]; public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index 9c19e9266..f006d2fd0 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -13,6 +13,15 @@ internal struct InlineArray3 private T first; } +/// +/// Used by JxlOpsinParameters +/// +[InlineArray(36)] +internal struct InlineArray36 +{ + private T first; +} + /// /// Used by JxlCustomTransformData /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs new file mode 100644 index 000000000..45c746324 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlDecoderCore : ImageDecoderCore +{ + /// + /// Identifies the signature of the JPEG XL file. + /// + private enum JxlSignature : byte + { + /// + /// Error status indicating not enough bytes to detect the signature. + /// + NotEnoughBytes, + + /// + /// A JPEG XL code stream. + /// + CodeStream, + + /// + /// The signature is invalid. + /// + Invalid, + + /// + /// Container format. + /// + Container + } + + /// + /// Represents a data type. + /// + private enum JxlDataType : byte + { + /// + /// + /// + UInt8, + + /// + /// + /// + UInt16, + + /// + /// + /// + Float, + + /// + /// + /// + Float16 + } + + public JxlDecoderCore(DecoderOptions options) + : base(options) + { + } + + /// + /// Ensures that the coordinates are not out of bounds. + /// + /// First coordinate + /// Second coordinate + /// Image width + /// Boolean indicating whether the coordinates are out of bounds + private static bool IsOutOfBounds(int a, int b, int size) + { + int position = a + b; + + return position > size || position < a; + } + + private static int InitialBasicInfoSizeHint() + { + const int containerHeaderSize = 48; + const int maxCodestreamBasicInfoSize = 50; + return containerHeaderSize + maxCodestreamBasicInfoSize; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + { + if (position >= length) + { + return JxlSignature.NotEnoughBytes; + } + + buffer = buffer[position..]; + length -= position; + + // 0xFF 0x0A represents a codestream + if (length >= 1 && buffer[0] == 0xFF) + { + if (length < 2) + { + // We need at least two bytes for a valid codestream signature + return JxlSignature.NotEnoughBytes; + } + else if (buffer[1] == CodestreamMarker) + { + position += 2; + return JxlSignature.CodeStream; + } + else + { + return JxlSignature.Invalid; + } + } + + // Container? + if (length >= 1 && buffer[0] == 0) + { + if (length < SignatureBox.Length) + { + return JxlSignature.NotEnoughBytes; + } + else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + { + position += SignatureBox.Length; + return JxlSignature.Container; + } + else + { + return JxlSignature.Invalid; + } + } + + // Signature is invalid + return JxlSignature.Invalid; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) + { + int position = 0; + return DetectSignature(buffer, length, ref position); + } + + private static int BitsPerChannel(JxlDataType dataType) + => dataType switch + { + JxlDataType.UInt8 => 8, + JxlDataType.UInt16 or JxlDataType.Float16 => 16, + JxlDataType.Float => 32, + _ => 0 + }; + + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); + + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs new file mode 100644 index 000000000..819517551 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal static class JxlNoiseDecoder +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BitsToFloatingPoint(ReadOnlySpan randomBits, Span floats) + { + Vector bits = new(randomBits); + Vector rand12 = ((bits >> 9) | new Vector(0x3F800000u)).As(); + rand12.StoreUnsafe(ref MemoryMarshal.GetReference(floats)); + } + + public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, JxlImageF noise) + { + const int floatsPerBatch = JxlXorShift.Generators * sizeof(ulong) / sizeof(float); + + int xSize = rectangle.Width; + int ySize = rectangle.Height; + + Span batch64 = stackalloc ulong[JxlXorShift.Generators]; + Span batch32 = stackalloc uint[JxlXorShift.Generators * 2]; + + // stackalloc doesn't zero-initialize, so clear values + batch64.Clear(); + batch32.Clear(); + + int n = Vector.Count; + + for (int y = 0; y < ySize; y++) + { + Span row = noise.GetRow(rectangle, y); + int x = 0; + for (; x + floatsPerBatch < xSize; x += floatsPerBatch) + { + rng.Fill(batch64); + MemoryMarshal.Cast(batch32).CopyTo(batch64); + for (int i = 0; i < floatsPerBatch; i += n) + { + BitsToFloatingPoint(batch32[i..], row[(x + i)..]); + } + } + + rng.Fill(batch64); + MemoryMarshal.Cast(batch32).CopyTo(batch64); + + int batchPos = 0; + + for (; x < xSize; x += n) + { + BitsToFloatingPoint(batch32[batchPos..], row[x..]); + batchPos += n; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs new file mode 100644 index 000000000..c18a18ef0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlOpsinParameters +{ + // Use arrays instead of InlineArrays because, with inline arrays we can't do: + // JxlOpsinParameters parameters = ...; + // parameters.OpsinBiasesCbrt[0] /* <-- error */ = 1.25f; + public float[] InverseOpsinMatrix { get; set; } = new float[36]; + + public float[] OpsinBiases { get; set; } = new float[4]; + + public float[] OpsinBiasesCbrt { get; set; } = new float[4]; + + public float[] QuantBiases { get; set; } = new float[4]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs new file mode 100644 index 000000000..bdec66643 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +// Prefer class instead of struct because it's too large for a struct +// Note that this struct wasn't well documented, so it's not that easy to add +// XML documentation here. +internal sealed class JxlOutputEncodingInfo +{ + public JxlColorEncoding? OriginalColorEncoding { get; set; } + + public float OriginalIntensityTarget { get; set; } + + public JxlMatrix3x3F OriginalInverseMatrix { get; set; } + + public bool DefaultTransform { get; set; } + + public bool XybEncoded { get; set; } + + /// + /// Gets or sets the requested color encoding. + /// + public JxlColorEncoding ColorEncoding { get; set; } = new(); + + public JxlColorEncoding LinearColorEncoding { get; set; } = new(); + + public bool ColorEncodingIsOriginal { get; set; } + + public JxlOpsinParameters OpsinParameters { get; set; } = new(); + + public bool AllDefaultOpsin { get; set; } + + public float InverseGamma { get; set; } + + public Vector3 Luminances { get; set; } + + public float DesiredIntensityTarget { get; set; } + + public bool CmsSet { get; set; } + + public JxlCmsInterface Cms { get; set; } + + public void SetFromMetadata(JxlCodecMetadata metadata) + { + JxlImageMetadata imageMetadata = metadata.ImageMetadata ?? throw new InvalidOperationException("Missing image metadata"); + + this.OriginalColorEncoding = imageMetadata.ColorEncoding; + this.OriginalIntensityTarget = imageMetadata.IntensityTarget; + this.DesiredIntensityTarget = this.OriginalIntensityTarget; + + JxlOpsinInverseMatrix inverseMatrix = metadata.CustomTransformData?.OpsinInverseMatrix ?? throw new InvalidOperationException("Missing Opsin inverse matrix or transform data"); + this.OriginalInverseMatrix = inverseMatrix.InverseMatrix; + this.DefaultTransform = inverseMatrix.AllDefault; + this.XybEncoded = imageMetadata.XybEncoded; + + JxlOpsinParameters parameters = this.OpsinParameters; + + imageMetadata.OpsinBiases.CopyTo(parameters.OpsinBiases); + parameters.OpsinBiasesCbrt[0] = MathF.Cbrt(parameters.OpsinBiases[0]); + parameters.OpsinBiasesCbrt[1] = MathF.Cbrt(parameters.OpsinBiases[1]); + parameters.OpsinBiasesCbrt[2] = MathF.Cbrt(parameters.OpsinBiases[2]); + + parameters.OpsinBiasesCbrt[3] = 1; + parameters.OpsinBiases[3] = 1; + + inverseMatrix.QuantBiases.AsSpan().CopyTo(parameters.QuantBiases); + + bool origOK = JxlXybDecoder.CanOutputToColorEncoding(this.OriginalColorEncoding ?? throw new InvalidCastException("Missing color encoding")); + bool origGrey = this.OriginalColorEncoding.IsGray; + + return this.SetColorEncoding(!this.XybEncoded || origOK ? this.OriginalColorEncoding : JxlColorEncoding.LinearSrgb(origGrey)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs new file mode 100644 index 000000000..940b01170 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Decodes the XYB color format (which JPEG XL uses) into RGB. +/// +internal static class JxlXybDecoder +{ + /// + /// Converts XYB to RGB using SIMD, one vector at a time. + /// + /// X channel + /// Y channel + /// B channel + /// Opsin parameters & configuration + /// Output R + /// Output G + /// Output B + public static void ConvertXybToRgb( + Vector opsinX, + Vector opsinY, + Vector opsinB, + JxlOpsinParameters opsinParameters, + ref Vector linearR, + ref Vector linearG, + ref Vector linearB) + { + Vector negBiasR = new(opsinParameters.OpsinBiaases[0]); + Vector negBiasG = new(opsinParameters.OpsinBiaases[1]); + Vector negBiasB = new(opsinParameters.OpsinBiaases[2]); + + Vector gammaR = opsinX + opsinY; + Vector gammaG = opsinY - opsinX; + Vector gammaB = opsinB; + + Vector gammaR2 = gammaR * gammaR; + Vector gammaG2 = gammaG * gammaG; + Vector gammaB2 = gammaB * gammaB; + + Vector mixedR = (gammaR2 * gammaR) + negBiasR; + Vector mixedG = (gammaG2 * gammaG) + negBiasG; + Vector mixedB = (gammaB2 * gammaB) + negBiasB; + + Span inverseMatrix = opsinParameters.GetInverseOpsinMatrixSpan(); + + linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR; + linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR; + linearB = LoadDuplicate128(ref inverseMatrix[6 * 4]) * mixedR; + + linearR = (LoadDuplicate128(ref inverseMatrix[1 * 4]) * mixedG) + linearR; + linearG = (LoadDuplicate128(ref inverseMatrix[4 * 4]) * mixedG) + linearG; + linearB = (LoadDuplicate128(ref inverseMatrix[7 * 4]) * mixedG) + linearB; + + linearR = (LoadDuplicate128(ref inverseMatrix[2 * 4]) * mixedB) + linearR; + linearG = (LoadDuplicate128(ref inverseMatrix[5 * 4]) * mixedB) + linearG; + linearB = (LoadDuplicate128(ref inverseMatrix[8 * 4]) * mixedB) + linearB; + } + + public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters) + { + if (!SameSize(rect, linear)) + { + return false; + } + + if (Vector.Count < 4) + { + // TODO: support 64bit vectors or no SIMD? + throw new PlatformNotSupportedException("XYB to RGB conversion requires at least 128-bit SIMD"); + } + + // Reuse variables instead of creating them over + // and over again + Unsafe.SkipInit(out Vector linearR); + Unsafe.SkipInit(out Vector linearG); + Unsafe.SkipInit(out Vector linearB); + + for (int y = 0; y < rect.Height; y++) + { + ReadOnlySpan rowOpsin0 = opsin.PlaneRow(rect, 0, y); + ReadOnlySpan rowOpsin1 = opsin.PlaneRow(rect, 1, y); + ReadOnlySpan rowOpsin2 = opsin.PlaneRow(rect, 2, y); + + ref float rowOpsin0Reference = ref MemoryMarshal.GetReference(rowOpsin0); + ref float rowOpsin1Reference = ref MemoryMarshal.GetReference(rowOpsin1); + ref float rowOpsin2Reference = ref MemoryMarshal.GetReference(rowOpsin2); + + Span rowLinear0 = linear.PlaneRow(0, y); + Span rowLinear1 = linear.PlaneRow(1, y); + Span rowLinear2 = linear.PlaneRow(2, y); + + ref float rowLinear0Reference = ref MemoryMarshal.GetReference(rowLinear0); + ref float rowLinear1Reference = ref MemoryMarshal.GetReference(rowLinear1); + ref float rowLinear2Reference = ref MemoryMarshal.GetReference(rowLinear2); + + for (int x = 0; x < rect.Height; x += Vector.Count) + { + Vector inOpsinX = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin0Reference, x)); + Vector inOpsinY = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin1Reference, x)); + Vector inOpsinB = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin2Reference, x)); + + ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, opsinParameters, ref linearR, ref linearG, ref linearB); + + linearR.StoreUnsafe(ref Unsafe.Add(ref rowLinear0Reference, x)); + linearG.StoreUnsafe(ref Unsafe.Add(ref rowLinear1Reference, x)); + linearB.StoreUnsafe(ref Unsafe.Add(ref rowLinear2Reference, x)); + } + } + + return true; + } + + /// + /// A SIMD utility method which reads next 128 bits + /// (which in this case happens to be next 4 floats), + /// and duplicates them to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 128bit or 256bit vectors). + /// + /// Reference to first element to load & duplicate. + /// Vector with first 128 bits duplicated across the vector width. + private static Vector LoadDuplicate128(ref float reference) + { + Span value = stackalloc float[Vector.Count]; + Span values128 = [ + reference, + Unsafe.Add(ref reference, 1), + Unsafe.Add(ref reference, 2), + Unsafe.Add(ref reference, 3) + ]; + + for (int i = 0; i < Vector.Count; i += 4) + { + values128[i..].CopyTo(value[i..]); + } + + return new(value); + } + + public static bool CanOutputToColorEncoding(JxlColorEncoding colorEncoding) + { + if (!colorEncoding.HaveFields) + { + return false; + } + + JxlCustomTransferFunction tf = colorEncoding.TransferFunction; + + if (!tf.IsPq && !tf.IsSrgb && !tf.HaveGamma && !tf.IsLinear && !tf.IsHlg && !tf.IsDci && !tf.Is709) + { + return false; + } + + if (colorEncoding.IsGray && colorEncoding.WhitePoint != JxlWhitePoint.D65) + { + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs new file mode 100644 index 000000000..ccea59b2c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Security.Principal; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Helps with reordering coefficients. +/// +internal static class JxlCoefficientOrder +{ + public const int Limit = 6156; + + public const int CoefficientOrderMaxSize = Limit * JxlFrameDimensions.DctBlockSize; + + public const int PermutationContexts = 8; + + /// + /// Gets the pattern which coefficients must follow to compute offsets. + /// + public static ReadOnlySpan CoefficientOrderOffsets => + [ + 0, 1, 2, 3, 4, 5, 6, 10, 14, 18, + 34, 50, 66, 68, 70, 72, 76, 80, 84, 92, + 100, 108, 172, 236, 300, 332, 364, 396, 652, 908, + 1164, 1292, 1420, 1548, 2572, 3596, 4620, 5132, 5644, Limit + ]; + + /// + /// Gets the pattern which coefficients must follow to compute offsets. + /// + public static ReadOnlySpan StrategyOrder => + [ + 0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1, + 1, 1, 1, 1, 7, 8, 8, 9, 10, 10, 11, 12, 12, + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CoeffOrderOffset(int o, int c) => CoefficientOrderOffsets[(3 * o) + c] * JxlFrameDimensions.DctBlockSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint CoeffOrderContext(uint value) + { + uint token = 0; + uint nbits = 0; + uint bits = 0; + + new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, ref token, ref nbits, ref bits); + + return Math.Min(token, PermutationContexts - 1u); + } + + public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) + { + Span lehmer = stackalloc uint[size]; + lehmer.Clear(); + + Span temp = stackalloc uint[size * 2]; + temp.Clear(); + + uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((int)size), bitReader, contextMap) + skip; + + if (end > size) + { + throw new InvalidOperationException("Invalid permutation size"); + } + + uint last = 0; + + for (int i = skip; i < end; i++) + { + lehmer[i] = reader.ReadHybridUnsignedInteger(CoeffOrderContext(last), bitReader, contextMap); + last = lehmer[i]; + if (lehmer[i] >= size - i) + { + throw new InvalidOperationException("Invalid lehmer code"); + } + } + + if (order.IsEmpty) + { + return true; + } + + return JxlLehmerCode.DecodeLehmerCode(lehmer, temp, size, order); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs new file mode 100644 index 000000000..9bf6a7dcb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -0,0 +1,424 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Convolution filters +/// +internal static class JxlConvolve +{ + /// + /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2]. + /// + public static float WeightedSumBorder( + JxlImageF input, + Func wrapY, + long ix, + long iy, + int width, + int height, + float wx0, + float wx1, + float wx2) + { + ReadOnlySpan row = input.GetRow(wrapY(iy, height)); + + float inM2 = row[WrapMirror(ix - 2, width)]; + float inP2 = row[WrapMirror(ix + 2, width)]; + float inM1 = row[WrapMirror(ix - 1, width)]; + float inP1 = row[WrapMirror(ix + 1, width)]; + float in00 = row[(int)ix]; + + float sum2 = wx2 * (inM2 + inP2); + float sum1 = wx1 * (inM1 + inP1); + float sum0 = wx0 * in00; + + return sum2 + (sum1 + sum0); + } + + public static Vector WeightedSum( + JxlImageF input, + Func wrapY, + int ix, + long iy, + int height, + Vector wx0, + Vector wx1, + Vector wx2) + { + ReadOnlySpan center = input.GetRow(wrapY(iy, height))[ix..]; + ref float centerRef = ref MemoryMarshal.GetReference(center); + + Vector inM2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 2)); + Vector inP2 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 2)); + Vector inM1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 1)); + Vector inP1 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 1)); + Vector in00 = Vector.LoadUnsafe(ref centerRef); + + Vector sum2 = wx2 * (inM2 + inP2); + Vector sum1 = wx1 * (inM1 + inP1); + Vector sum0 = wx0 * in00; + + return sum2 + (sum1 + sum0); + } + + public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, JxlWeightsSymmetric5 weights) + { + float w0 = weights.GetCVector()[0]; + float w1 = weights.GetRVector()[0]; + float w2 = weights.GetR2Vector()[0]; + float w4 = weights.GetDVector()[0]; + float w5 = weights.GetCVector()[0]; + float w8 = weights.GetD2Vector()[0]; + + int width = input.XSize; + int height = input.YSize; + + float sum0 = WeightedSumBorder(input, wrapY, ix, iy, width, height, w0, w1, w2) + + WeightedSumBorder(input, wrapY, ix, iy - 2, width, height, w2, w5, w8); + + float sum1 = WeightedSumBorder(input, wrapY, ix, iy + 2, width, height, w2, w5, w8); + + sum0 += WeightedSumBorder(input, wrapY, ix, iy + 1, width, height, w1, w4, w5); + sum1 += WeightedSumBorder(input, wrapY, ix, iy - 1, width, height, w1, w4, w5); + + return sum0 + sum1; + } + + public static void Symmetric5Interior( + JxlImageF image, + int ix, + Func wrapY, + int rix, + long iy, + JxlWeightsSymmetric5 weights, + Span rowOut) + { + Vector w0 = LoadDuplicate128(weights.GetCVector()); // c + Vector w1 = LoadDuplicate128(weights.GetRVector()); // r + Vector w2 = LoadDuplicate128(weights.GetR2Vector()); // R + Vector w4 = LoadDuplicate128(weights.GetDVector()); // d + Vector w5 = LoadDuplicate128(weights.GetLVector()); // L + Vector w8 = LoadDuplicate128(weights.GetD2Vector()); // D + + int height = image.YSize; + Vector sum0 = WeightedSum(image, wrapY, ix, iy, height, w0, w1, w2) + + WeightedSum(image, wrapY, ix, iy - 2, height, w2, w5, w8); + + Vector sum1 = WeightedSum(image, wrapY, ix, iy + 2, height, w2, w5, w8); + + sum0 += WeightedSum(image, wrapY, ix, iy - 1, height, w1, w4, w5); + sum1 += WeightedSum(image, wrapY, ix, iy + 1, height, w1, w4, w5); + + (sum0 + sum1).StoreUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(rowOut), rix)); + } + + public static void Symmetric5Row( + JxlImageF image, + Func wrapY, + in Rectangle rect, + long iy, + JxlWeightsSymmetric5 weights, + Span rowOut) + { + const int radius = 2; + int xEnd = rect.Right; + + int rix = 0; + int ix = rect.X; + + int n = Vector.Count; + int alignedX = RoundUpTo(radius, n); + + for (; ix < Math.Min(alignedX, xEnd); ix++, rix++) + { + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + } + + for (; ix + n + radius <= xEnd; ix += n, rix += n) + { + Symmetric5Interior(image, ix, wrapY, rix, iy, weights, rowOut); + } + + for (; ix < xEnd; ix++, rix++) + { + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + } + } + + public static bool Symmetric5( + JxlImageF input, + in Rectangle rectangle, + JxlWeightsSymmetric5 weights, + JxlImageF output, + Rectangle outputRect) + { + if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) + { + return false; + } + + int height = rectangle.Height; + + for (int riy = 0; riy < height; riy++) + { + int iy = rectangle.Y + riy; + + if (iy < 2 || iy >= rectangle.Height - 2) + { + Symmetric5Row(input, WrapMirror, in rectangle, iy, weights, output.GetRow(outputRect, riy)); + } + else + { + Symmetric5Row(input, in rectangle, iy, weights, output.GetRow(outputRect, riy)); + } + } + + return true; + } + + public static float SlowSymmetric3Pixel( + JxlImageF image, + int x, + int y, + int width, + int height, + JxlWeightsSymmetric3 weights, + Func wrapX, + Func wrapY) + { + float sum = 0.0f; + + float c0 = weights.GetCVector()[0]; + float r0 = weights.GetRVector()[0]; + float d0 = weights.GetDVector()[0]; + + for (int ky = -1; ky <= 1; ky++) + { + int yy = wrapY(y + ky, height); + ReadOnlySpan row = image.GetRow(yy); + + float wc = (ky == 0) ? c0 : r0; + float wlr = (ky == 0) ? r0 : d0; + + int xm1 = wrapX(x - 1, width); + int xp1 = wrapX(x + 1, width); + + sum += (row[x] * wc) + ((row[xm1] + row[xp1]) * wlr); + } + + return sum; + } + + public static void SlowSymmetric3Row( + JxlImageF image, + int y, + int width, + int height, + JxlWeightsSymmetric3 weights, + Span outputRow, + Func wrapY) + { + outputRow[0] = SlowSymmetric3Pixel( + image, + 0, + y, + width, + height, + weights, + WrapMirror, + wrapY); + + for (int x = 1; x < width - 1; x++) + { + outputRow[x] = SlowSymmetric3Pixel( + image, + x, + y, + width, + height, + weights, + WrapUnchanged, + wrapY); + } + + outputRow[width - 1] = SlowSymmetric3Pixel( + image, + width - 1, + y, + width, + height, + weights, + WrapMirror, + wrapY); + } + + public static void SlowSymmetric3( + JxlImageF input, + Rectangle rect, + JxlWeightsSymmetric3 weights, + JxlImageF output) + { + int width = rect.Width; + int height = rect.Height; + + const int radius = 1; + + for (int y = 0; y < height; y++) + { + Span rowOut = output.GetRow(y); + + if (y < radius || y >= height - radius) + { + SlowSymmetric3Row( + input, + y, + width, + height, + weights, + rowOut, + WrapMirror); + } + else + { + SlowSymmetric3Row( + input, + y, + width, + height, + weights, + rowOut, + WrapUnchanged); + } + } + } + + public static float SlowSeparablePixel( + JxlImageF image, + Rectangle rect, + int x, + int y, + int radius, + ReadOnlySpan horzWeights, + ReadOnlySpan vertWeights) + { + int width = image.XSize; + int height = image.YSize; + + float sum = 0; + + for (int dy = -radius; dy <= radius; dy++) + { + float wy = vertWeights[Math.Abs(dy) * 4]; + int sy = WrapMirror(rect.Y + y + dy, height); + ReadOnlySpan row = image.GetRow(sy); + + for (int dx = -radius; dx <= radius; dx++) + { + float wx = horzWeights[Math.Abs(dx) * 4]; + int sx = WrapMirror(rect.X + x + dx, width); + sum += row[sx] * wx * wy; + } + } + + return sum; + } + + public static void SlowSeparable( + JxlImageF input, + Rectangle inputRect, + JxlWeightsSeparable5 weights, + JxlImageF output, + Rectangle outputRect, + int radius) + { + ReadOnlySpan horz = weights.Horizontal; + ReadOnlySpan vert = weights.Vertical; + + for (int y = 0; y < inputRect.Height; y++) + { + Span rowOut = output.GetRow(outputRect, y); + + for (int x = 0; x < inputRect.Width; x++) + { + rowOut[x] = SlowSeparablePixel( + input, + inputRect, + x, + y, + radius, + horz, + vert); + } + } + } + + public static void SlowSeparable5( + JxlImageF input, + Rectangle inputRect, + JxlWeightsSeparable5 weights, + JxlImageF output, + Rectangle outputRect) + => SlowSeparable(input, inputRect, weights, output, outputRect, 2); + + public static void FirstL1(ReadOnlySpan c, Span dst) + { + dst[0] = c[0]; + for (int i = 1; i < dst.Length; i++) + { + dst[i] = c[i - 1]; + } + } + + public static void FirstL2(ReadOnlySpan c, Span dst) + { + dst[0] = c[1]; + dst[1] = c[0]; + + for (int i = 2; i < dst.Length; i++) + { + dst[i] = c[i - 2]; + } + } + + /// + /// A SIMD utility method which takes in the 128 bit vector + /// and duplicates its values to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 256bit vectors). + /// + /// Vector to duplicate. + /// New vector that is duplicated across the width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector LoadDuplicate128(Vector128 vec) + { + Span value = stackalloc float[Vector.Count]; + for (int i = 0; i < Vector.Count; i += 4) + { + vec.CopyTo(value[i..]); + } + + return new(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs index 84e7819bc..26bc7ee8e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// /// Read-only cosine lookups for the Discrete Cosine Transform (DCT), -/// a mathematical function used for quantization. +/// a mathematical function used for quantization and coefficient reordering. /// internal static class JxlDctScales { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs new file mode 100644 index 000000000..21fa5d23b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -0,0 +1,191 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Table of Contents encoding +/// +internal static class JxlToc +{ + private static readonly JxlU32Enc TocDistribution = new( + JxlFieldExpressions.Bits(10), + JxlFieldExpressions.BitsOffset(14, 2024), + JxlFieldExpressions.BitsOffset(22, 17408), + JxlFieldExpressions.BitsOffset(30, 4211712)); + + public static int AcGroupIndex(int pass, int group, int numGroups, int numDcGroups) + => 2 + numDcGroups + (pass * numGroups) + group; + + public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPasses) + { + if (numGroups == 1 && numPasses == 1) + { + return 1; + } + + return AcGroupIndex(0, 0, numGroups, numDcGroups) + (numGroups * numPasses); + } + + private const int BitsPerByte = 8; + private const int MaxTocEntries = 65536; + + public static bool ReadToc( + Configuration configuration, + int tocEntries, + JxlBitReader reader, + List sizes, + List permutation) + { + if (tocEntries > MaxTocEntries) + { + return false; // too many TOC entries + } + + sizes.Clear(); + sizes.Capacity = tocEntries; + + for (int i = 0; i < tocEntries; i++) + { + sizes.Add(0); + } + + if (reader.TotalBitsConsumed >= reader.TotalBytes * BitsPerByte) + { + return false; // not enough bytes + } + + bool CheckBitBudget(int numEntries) + { + long minimalBitCost = numEntries * (2 + 10); + long bitBudget = reader.TotalBytes * BitsPerByte; + long expenses = reader.TotalBitsConsumed; + + return expenses <= bitBudget && + minimalBitCost <= bitBudget - expenses; + } + + if (tocEntries <= 0) + { + return false; + } + + if (reader.ReadBits32(1) == 1) + { + if (!CheckBitBudget(tocEntries)) + { + return false; + } + + permutation.Clear(); + + for (int i = 0; i < tocEntries; i++) + { + permutation.Add(default); + } + + if (!DecodePermutation( + configuration, + 0, + tocEntries, + permutation, + reader)) + { + return false; + } + } + + if (!reader.JumpToByteBoundary()) + { + return false; + } + + if (!CheckBitBudget(tocEntries)) + { + return false; + } + + for (int i = 0; i < tocEntries; i++) + { + sizes[i] = JxlU32Coder.Read(TocDistribution, reader); + } + + if (!reader.JumpToByteBoundary()) + { + return false; + } + + return CheckBitBudget(0); + } + + public static bool ReadGroupOffsets( + Configuration configuration, + int tocEntries, + JxlBitReader reader, + List offsets, + List sizes, + out ulong totalSize) + { + totalSize = 0; + + List permutation = []; + + if (!ReadToc( + configuration, + tocEntries, + reader, + sizes, + permutation)) + { + return false; + } + + offsets.Clear(); + offsets.Capacity = tocEntries; + + for (int i = 0; i < tocEntries; i++) + { + offsets.Add(0); + } + + ulong offset = 0; + + for (int i = 0; i < tocEntries; i++) + { + ulong size = sizes[i]; + + if (offset + size < offset) + { + return false; + } + + offsets[i] = offset; + offset += size; + } + + totalSize = offset; + + if (permutation.Count != 0) + { + List permutedOffsets = new(tocEntries); + List permutedSizes = new(tocEntries); + + foreach (byte index in permutation) + { + permutedOffsets.Add(offsets[index]); + permutedSizes.Add(sizes[index]); + } + + offsets.Clear(); + offsets.AddRange(permutedOffsets); + + sizes.Clear(); + sizes.AddRange(permutedSizes); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs index b3849d69f..78d8900a5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs @@ -7,6 +7,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlXorShift { + public const int Generators = 8; + private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8];