From 649034b023b685d084800b057715287d0eab8581 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Mon, 7 Sep 2026 00:50:51 +0400
Subject: [PATCH] Add PQ transfer function
---
...ceptualQuantizationTransferFunctionBase.cs | 54 -------
.../JxlPqTransferFunction.cs | 142 ++++++++++++++++++
2 files changed, 142 insertions(+), 54 deletions(-)
delete mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs
deleted file mode 100644
index eea7b06560..0000000000
--- a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
-
-///
-/// Base class for PQ transfer function.
-///
-internal abstract class JxlPerceptualQuantizationTransferFunctionBase
-{
- private const double M1 = 2610.0 / 16384;
- private const double M2 = (2523.0 / 4096) * 128;
- private const double C1 = 3424.0 / 4096;
- private const double C2 = (2413.0 / 4096) * 32;
- private const double C3 = (2392.0 / 4096) * 32;
-
- protected static double DisplayFromEncoded(float displayIntensityTarget, double e)
- {
- if (e == 0.0)
- {
- return 0.0;
- }
-
- double originalSign = e;
-
- e = Math.Abs(e);
-
- double xp = Math.Pow(e, 1.0 / M2);
- double num = Math.Max(xp - C1, 0.0);
- double den = C2 - (C3 * xp);
- double d = Math.Pow(num / den, 1.0 / M1);
-
- return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign);
- }
-
- protected static double EncodedFromDisplay(float displayIntensityTarget, double d)
- {
- if (d == 0.0)
- {
- return 0.0;
- }
-
- double originalSign = d;
-
- d = Math.Abs(d);
-
- double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1);
- double num = C1 + (xp * C2);
- double den = 1.0 + (xp * C3);
- double e = Math.Pow(num / den, M2);
-
- return Math.CopySign(e, originalSign);
- }
-}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs
new file mode 100644
index 0000000000..654d05429b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs
@@ -0,0 +1,142 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
+
+///
+/// Perceptual Quantization transfer function.
+///
+internal struct JxlPqTransferFunction(float displayIntensityTarget = DefaultIntensityTarget)
+{
+ private const double M1 = 2610.0 / 16384;
+ private const double M2 = (2523.0 / 4096) * 128;
+ private const double C1 = 3424.0 / 4096;
+ private const double C2 = (2413.0 / 4096) * 32;
+ private const double C3 = (2392.0 / 4096) * 32;
+
+ private readonly float scaleFactorTo10000Nits = displayIntensityTarget * (1.0f / 10000.0f);
+ private readonly float scaleFactorFrom10000Nits = 10000.0f / displayIntensityTarget;
+
+ private static ReadOnlySpan DisplayP =>
+ [
+ 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f,
+ -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f,
+ 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f,
+ 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f,
+ 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f
+ ];
+
+ private static ReadOnlySpan DisplayQ =>
+ [
+ 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f,
+ -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f,
+ 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f,
+ -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f,
+ 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f
+ ];
+
+ private static ReadOnlySpan EncodedP =>
+ [
+ 1.351392e-02f, 1.351392e-02f, 1.351392e-02f, 1.351392e-02f,
+ -1.095778e+00f, -1.095778e+00f, -1.095778e+00f, -1.095778e+00f,
+ 5.522776e+01f, 5.522776e+01f, 5.522776e+01f, 5.522776e+01f,
+ 1.492516e+02f, 1.492516e+02f, 1.492516e+02f, 1.492516e+02f,
+ 4.838434e+01f, 4.838434e+01f, 4.838434e+01f, 4.838434e+01f
+ ];
+
+ private static ReadOnlySpan EncodedQ =>
+ [
+ 1.012416e+00f, 1.012416e+00f, 1.012416e+00f, 1.012416e+00f,
+ 2.016708e+01f, 2.016708e+01f, 2.016708e+01f, 2.016708e+01f,
+ 9.263710e+01f, 9.263710e+01f, 9.263710e+01f, 9.263710e+01f,
+ 1.120607e+02f, 1.120607e+02f, 1.120607e+02f, 1.120607e+02f,
+ 2.590418e+01f, 2.590418e+01f, 2.590418e+01f, 2.590418e+01f
+ ];
+
+ private static ReadOnlySpan EncodedPLo =>
+ [
+ 9.863406e-06f, 9.863406e-06f, 9.863406e-06f, 9.863406e-06f,
+ 3.881234e-01f, 3.881234e-01f, 3.881234e-01f, 3.881234e-01f,
+ 1.352821e+02f, 1.352821e+02f, 1.352821e+02f, 1.352821e+02f,
+ 6.889862e+04f, 6.889862e+04f, 6.889862e+04f, 6.889862e+04f,
+ -2.864824e+05f, -2.864824e+05f, -2.864824e+05f, -2.864824e+05f
+ ];
+
+ private static ReadOnlySpan EncodedQLo =>
+ [
+ 3.371868e+01f, 3.371868e+01f, 3.371868e+01f, 3.371868e+01f,
+ 1.477719e+03f, 1.477719e+03f, 1.477719e+03f, 1.477719e+03f,
+ 1.608477e+04f, 1.608477e+04f, 1.608477e+04f, 1.608477e+04f,
+ -4.389884e+04f, -4.389884e+04f, -4.389884e+04f, -4.389884e+04f,
+ -2.072546e+05f, -2.072546e+05f, -2.072546e+05f, -2.072546e+05f
+ ];
+
+ public static double DisplayFromEncoded(float displayIntensityTarget, double e)
+ {
+ if (e == 0.0)
+ {
+ return 0.0;
+ }
+
+ double originalSign = e;
+
+ e = Math.Abs(e);
+
+ double xp = Math.Pow(e, 1.0 / M2);
+ double num = Math.Max(xp - C1, 0.0);
+ double den = C2 - (C3 * xp);
+ double d = Math.Pow(num / den, 1.0 / M1);
+
+ return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign);
+ }
+
+ public static double EncodedFromDisplay(float displayIntensityTarget, double d)
+ {
+ if (d == 0.0)
+ {
+ return 0.0;
+ }
+
+ double originalSign = d;
+
+ d = Math.Abs(d);
+
+ double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1);
+ double num = C1 + (xp * C2);
+ double den = 1.0 + (xp * C3);
+ double e = Math.Pow(num / den, M2);
+
+ return Math.CopySign(e, originalSign);
+ }
+
+ public Vector DisplayFromEncoded(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+
+ Vector xpxx = (x * x) + x;
+ Vector magnitude = EvaluateRationalPolynomial(xpxx, DisplayP, DisplayQ);
+
+ return Vector.AndNot(sign, magnitude * Vector.Create(this.scaleFactorFrom10000Nits)) | originalSign;
+ }
+
+ public Vector EncodedFromDisplay(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+
+
+ Vector xto025 = Vector.SquareRoot(Vector.SquareRoot(x * Vector.Create(this.scaleFactorTo10000Nits)));
+
+ Vector magnitude = Vector.ConditionalSelect(
+ Vector.LessThan(x, Vector.Create(1e-4f)),
+ EvaluateRationalPolynomial(xto025, EncodedPLo, EncodedQLo),
+ EvaluateRationalPolynomial(xto025, EncodedP, EncodedQ));
+
+ return Vector.AndNot(sign, magnitude) | originalSign;
+ }
+}