From 16c55f343ad805e916cb39e2e366e4ab8b92b890 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Mon, 20 Jul 2026 01:43:13 +0400
Subject: [PATCH] Add frame header
---
.../Formats/Jxl/Fields/JxlF16Coder.cs | 1 -
.../Formats/Jxl/Fields/JxlU32Enc.cs | 6 +-
.../Formats/Jxl/Fields/JxlU64Coder.cs | 1 +
.../Jxl/IO/FrameHeader/JxlAnimationFrame.cs | 74 ++
.../Jxl/IO/FrameHeader/JxlBlendMode.cs | 64 ++
.../Jxl/IO/FrameHeader/JxlBlendingInfo.cs | 146 ++++
.../Jxl/IO/FrameHeader/JxlColorTransform.cs | 26 +
.../FrameHeader/JxlColorTransformHelpers.cs | 39 +
.../Jxl/IO/FrameHeader/JxlFrameEncoding.cs | 20 +
.../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 743 ++++++++++++++++++
.../Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs | 36 +
.../Jxl/IO/FrameHeader/JxlFrameType.cs | 37 +
.../Formats/Jxl/IO/FrameHeader/JxlPasses.cs | 220 ++++++
.../JxlYCbCrChromaSubsampling.cs | 2 +-
14 files changed, 1410 insertions(+), 5 deletions(-)
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
rename src/ImageSharp/Formats/Jxl/IO/{ => FrameHeader}/JxlYCbCrChromaSubsampling.cs (98%)
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
index 7bf83e1854..3c4126c4b8 100644
--- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Jxl.IO;
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
index 9583f57dac..5b6e10b847 100644
--- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Diagnostics;
-
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
internal readonly struct JxlU32Enc
@@ -19,7 +17,9 @@ internal readonly struct JxlU32Enc
public JxlU32Distribution GetDistribution(int selector)
{
- Debug.Assert(selector < 4, "Selector out of range");
+ // This stuff is internal, so if argument check
+ // fails it's not a user error.
+ DebugGuard.MustBeLessThan(selector, 4, nameof(selector));
return this.d[selector];
}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
index 6f5fb8b281..ba121cafe2 100644
--- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
@@ -83,6 +83,7 @@ internal static class JxlU64Coder
value >>= 8;
shift += 8;
}
+
if (value > 0)
{
// 1 continuation bit + 4 payload bits
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
new file mode 100644
index 0000000000..1854b4ab67
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Describes duration of frames that make up an animation.
+///
+internal sealed class JxlAnimationFrame : IJxlFields
+{
+ ///
+ /// See .
+ ///
+ private uint duration;
+
+ ///
+ /// See .
+ ///
+ private uint timecode;
+
+ ///
+ /// Gets or sets the duration of the animation.
+ ///
+ public uint Duration
+ {
+ get => this.duration;
+ set => this.duration = value;
+ }
+
+ ///
+ /// Gets or sets the timecode of the animation. The
+ /// format is 0xHHMMSSFF.
+ ///
+ public uint Timecode
+ {
+ get => this.timecode;
+ set => this.timecode = value;
+ }
+
+ ///
+ /// Gets or sets the optional codec metadata.
+ ///
+ public JxlCodecMetadata? CodecMetadata { get; set; }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Bits(8),
+ JxlFieldExpressions.Bits(32),
+ 0,
+ ref this.duration))
+ {
+ return false;
+ }
+ }
+
+ if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true))
+ {
+ if (!visitor.Bits(32, 0u, ref this.timecode))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
new file mode 100644
index 0000000000..035a48edd5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the blending mode describing how to combine
+/// current frame with previously saved frame.
+///
+internal enum JxlBlendMode : byte
+{
+ ///
+ /// New values replace old ones.
+ ///
+ /// sample = new
+ ///
+ ///
+ Replace,
+
+ ///
+ /// New values add to the old ones.
+ ///
+ /// sample = old + new
+ ///
+ ///
+ Add,
+
+ ///
+ /// New values replace old ones if alpha>0:
+ ///
+ /// alpha = old + new * (1 - old)
+ ///
+ /// For other channels if !alpha_associated:
+ ///
+ /// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha
+ ///
+ /// For other channels if alpha_associated:
+ ///
+ /// sample = (1 - newAlpha) * old + new
+ ///
+ ///
+ Blend,
+
+ ///
+ /// New values are added to the old ones if alpha>0:
+ /// For the alpha channel that is used as source:
+ ///
+ /// sample = old + new * (1 - old)
+ ///
+ /// Otherwise:
+ ///
+ /// sample = old + alpha * new
+ ///
+ ///
+ AlphaWeightedBlend,
+
+ ///
+ /// New values are multiplied by old ones:
+ ///
+ /// sample = old * new
+ ///
+ ///
+ Multiply
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
new file mode 100644
index 0000000000..58c79d22db
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Provides options and instructions that tell the decoder the proper
+/// way to blend the current and previous frame together.
+///
+internal sealed class JxlBlendingInfo : IJxlFields
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JxlBlendingInfo() => JxlBundle.Init(this);
+
+ ///
+ /// Gets or sets the blending mode. See .
+ ///
+ public JxlBlendMode BlendMode { get; set; }
+
+ ///
+ /// Gets or sets the value that indicates which extra channel
+ /// to use as alpha channel for blending.
+ ///
+ public uint AlphaChannel { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the alpha or channel values
+ /// must be clamped* to the 0 through 1 range.
+ ///
+ ///
+ /// Clamped - must be limited to the specified range.
+ ///
+ public bool Clamp { get; set; }
+
+ ///
+ /// Gets or sets the frame ID to copy from (0 through 3).
+ ///
+ ///
+ /// If is equal to ,
+ /// the value of this property is ignored.
+ ///
+ public uint Source { get; set; }
+
+ ///
+ /// Gets or sets the total number of extra channels.
+ ///
+ public int ExtraChannelCount { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the frame is partial.
+ ///
+ public bool IsPartialFrame { get; set; }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ JxlBlendMode mode = this.BlendMode;
+ if (!VisitBlendMode(visitor, JxlBlendMode.Replace, ref mode))
+ {
+ return false;
+ }
+
+ this.BlendMode = mode;
+
+ if (visitor.Conditional(this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend))
+ {
+ uint alphaChannel = this.AlphaChannel;
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0u),
+ JxlFieldExpressions.Value(1u),
+ JxlFieldExpressions.Value(2u),
+ JxlFieldExpressions.BitsOffset(3u, 3u),
+ 0,
+ ref alphaChannel))
+ {
+ return false;
+ }
+
+ this.AlphaChannel = alphaChannel;
+
+ if (visitor.IsReading && alphaChannel >= this.ExtraChannelCount)
+ {
+ throw new InvalidOperationException("Invalid alpha channel for blending");
+ }
+ }
+
+ if (visitor.Conditional((this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend) || mode == JxlBlendMode.Multiply))
+ {
+ bool clamp = this.Clamp;
+
+ if (!visitor.Boolean(false, ref clamp))
+ {
+ return false;
+ }
+
+ this.Clamp = clamp;
+ }
+
+ if (visitor.Conditional(mode != JxlBlendMode.Replace || this.IsPartialFrame))
+ {
+ uint source = this.Source;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref source))
+ {
+ return false;
+ }
+
+ this.Source = source;
+ }
+
+ return true;
+ }
+
+ private static bool VisitBlendMode(JxlVisitor visitor, JxlBlendMode defaultValue, ref JxlBlendMode valueToEncode)
+ {
+ uint unsignedBackingValue = (uint)valueToEncode;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Replace),
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Add),
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Blend),
+ JxlFieldExpressions.BitsOffset(2u, 3u),
+ (uint)defaultValue,
+ ref unsignedBackingValue))
+ {
+ return false;
+ }
+
+ if (unsignedBackingValue > (uint)JxlBlendMode.Multiply)
+ {
+ throw new InvalidOperationException("Invalid blend mode");
+ }
+
+ valueToEncode = (JxlBlendMode)unsignedBackingValue;
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
new file mode 100644
index 0000000000..f874983486
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the type of JPEG XL color transform.
+///
+internal enum JxlColorTransform : byte
+{
+ ///
+ /// Use XYB encoding
+ ///
+ Xyb,
+
+ ///
+ /// Encode according to the attached color profile.
+ ///
+ None,
+
+ ///
+ /// Encode according to the attached color profile but
+ /// transformed into Y'Cb'Cr.
+ ///
+ YCbCr,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
new file mode 100644
index 0000000000..de61664146
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Helper methods associated with JxlColorTransform.
+///
+internal static class JxlColorTransformHelpers
+{
+ private static readonly int[][] JpegOrders =
+ [
+ [0, 0, 0], // Grayscale
+ [1, 0, 2], // Y'Cb'Cr
+ [0, 1, 2], // None
+ [0, 1, 2] // Anything else
+ ];
+
+ public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle)
+ {
+ if (isGraysacle)
+ {
+ return JpegOrders[0];
+ }
+
+ if (transform == JxlColorTransform.YCbCr)
+ {
+ return JpegOrders[1];
+ }
+ else if (transform == JxlColorTransform.None)
+ {
+ return JpegOrders[2];
+ }
+ else
+ {
+ return JpegOrders[3];
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
new file mode 100644
index 0000000000..77ae964ac9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the kind of frame encoding.
+///
+internal enum JxlFrameEncoding : byte
+{
+ ///
+ /// Use VarDCT
+ ///
+ VarDct,
+
+ ///
+ /// Use Modular encoding
+ ///
+ Modular
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
new file mode 100644
index 0000000000..a45c0e4710
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
@@ -0,0 +1,743 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+// Disable IDE0032 for consistency with other fields.
+// We have to avoid auto properties for most fields
+// so we can use the ref keyword on them directly.
+#pragma warning disable IDE0032 // Use auto property
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Metadata;
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Control information for a JPEG XL frame.
+///
+internal sealed class JxlFrameHeader : IJxlFields
+{
+ // The following are backing fields for properties.
+ private JxlFrameEncoding encoding = JxlFrameEncoding.Modular;
+ private JxlFrameType frameType = JxlFrameType.RegularFrame;
+ private ulong flags;
+ private JxlColorTransform colorTransform = JxlColorTransform.Xyb;
+ private JxlYCbCrChromaSubsampling? chromaSubsampling;
+ private uint groupSizeShift;
+ private uint xQmScale;
+ private uint bQmScale;
+ private string? name;
+ private bool customSizeOrOrigin;
+ private Size frameSize;
+ private uint upsampling;
+ private List extraChannelUpsampling = [];
+ private Point frameOrigin;
+ private JxlBlendingInfo? blendingInfo;
+ private List extraChannelBlendingInfo = [];
+ private readonly JxlAnimationFrame? animationFrame;
+ private bool isLast;
+ private uint saveAsReference;
+ private bool saveBeforeColorTransform;
+ private uint dcLevel;
+ private JxlCodecMetadata? metadata;
+ private JxlLoopFilter? loopFilter;
+ private ulong extensions;
+
+ private bool isPreviewFrame; // Non-serialized
+
+ ///
+ /// Gets or sets the frame encoding method (e.g., Modular or VarDCT).
+ ///
+ public JxlFrameEncoding Encoding
+ {
+ get => this.encoding;
+ set => this.encoding = value;
+ }
+
+ ///
+ /// Gets or sets the type of frame (e.g., RegularFrame).
+ ///
+ public JxlFrameType FrameType
+ {
+ get => this.frameType;
+ set => this.frameType = value;
+ }
+
+ ///
+ /// Gets or sets the frame flags.
+ ///
+ public ulong Flags
+ {
+ get => this.flags;
+ set => this.flags = value;
+ }
+
+ ///
+ /// Gets or sets the color transform used (e.g., XYB).
+ ///
+ public JxlColorTransform ColorTransform
+ {
+ get => this.colorTransform;
+ set => this.colorTransform = value;
+ }
+
+ ///
+ /// Gets or sets the chroma subsampling information.
+ ///
+ public JxlYCbCrChromaSubsampling? ChromaSubsampling
+ {
+ get => this.chromaSubsampling;
+ set => this.chromaSubsampling = value;
+ }
+
+ ///
+ /// Gets or sets the group size shift value.
+ ///
+ public uint GroupSizeShift
+ {
+ get => this.groupSizeShift;
+ set => this.groupSizeShift = value;
+ }
+
+ ///
+ /// Gets or sets the X quantization matrix scale.
+ ///
+ public uint XQmScale
+ {
+ get => this.xQmScale;
+ set => this.xQmScale = value;
+ }
+
+ ///
+ /// Gets or sets the B quantization matrix scale.
+ ///
+ public uint BQmScale
+ {
+ get => this.bQmScale;
+ set => this.bQmScale = value;
+ }
+
+ ///
+ /// Gets or sets the frame name.
+ ///
+ public string? Name
+ {
+ get => this.name;
+ set => this.name = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the frame has a custom size or origin.
+ ///
+ public bool CustomSizeOrOrigin
+ {
+ get => this.customSizeOrOrigin;
+ set => this.customSizeOrOrigin = value;
+ }
+
+ ///
+ /// Gets or sets the frame size.
+ ///
+ public Size FrameSize
+ {
+ get => this.frameSize;
+ set => this.frameSize = value;
+ }
+
+ ///
+ /// Gets or sets the upsampling factor.
+ ///
+ public uint Upsampling
+ {
+ get => this.upsampling;
+ set => this.upsampling = value;
+ }
+
+ ///
+ /// Gets or sets the upsampling factors for extra channels.
+ ///
+ public List ExtraChannelUpsampling
+ {
+ get => this.extraChannelUpsampling;
+ set => this.extraChannelUpsampling = value;
+ }
+
+ ///
+ /// Gets or sets the frame origin point.
+ ///
+ public Point FrameOrigin
+ {
+ get => this.frameOrigin;
+ set => this.frameOrigin = value;
+ }
+
+ ///
+ /// Gets or sets the blending information for the frame.
+ ///
+ public JxlBlendingInfo? BlendingInfo
+ {
+ get => this.blendingInfo;
+ set => this.blendingInfo = value;
+ }
+
+ ///
+ /// Gets or sets the blending information for extra channels.
+ ///
+ public List ExtraChannelBlendingInfo
+ {
+ get => this.extraChannelBlendingInfo;
+ set => this.extraChannelBlendingInfo = value;
+ }
+
+ ///
+ /// Gets the associated animation frame, if any.
+ ///
+ public JxlAnimationFrame? AnimationFrame => this.animationFrame;
+
+ ///
+ /// Gets or sets a value indicating whether this is the last frame.
+ ///
+ public bool IsLast
+ {
+ get => this.isLast;
+ set => this.isLast = value;
+ }
+
+ ///
+ /// Gets or sets the reference frame index to save.
+ ///
+ public uint SaveAsReference
+ {
+ get => this.saveAsReference;
+ set => this.saveAsReference = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to save before color transform.
+ ///
+ public bool SaveBeforeColorTransform
+ {
+ get => this.saveBeforeColorTransform;
+ set => this.saveBeforeColorTransform = value;
+ }
+
+ ///
+ /// Gets or sets the DC level of the frame.
+ ///
+ public uint DcLevel
+ {
+ get => this.dcLevel;
+ set => this.dcLevel = value;
+ }
+
+ ///
+ /// Gets or sets the codec metadata.
+ ///
+ public JxlCodecMetadata? Metadata
+ {
+ get => this.metadata;
+ set => this.metadata = value;
+ }
+
+ ///
+ /// Gets or sets the loop filter applied to the frame.
+ ///
+ public JxlLoopFilter? LoopFilter
+ {
+ get => this.loopFilter;
+ set => this.loopFilter = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this is a preview frame. Non-serialized.
+ ///
+ public bool IsPreviewFrame
+ {
+ get => this.isPreviewFrame;
+ set => this.isPreviewFrame = value;
+ }
+
+ ///
+ /// Gets or sets the number of extensions.
+ ///
+ public ulong Extensions
+ {
+ get => this.extensions;
+ set => this.extensions = value;
+ }
+
+ public int DefaultXSize
+ {
+ get
+ {
+ if (this.metadata == null)
+ {
+ return 0;
+ }
+
+ if (this.isPreviewFrame)
+ {
+ return this.metadata.ImageMetadata?.PreviewSize?.XSize ?? 0;
+ }
+
+ return this.metadata.XSize;
+ }
+ }
+
+ public int DefaultYSize
+ {
+ get
+ {
+ if (this.metadata == null)
+ {
+ return 0;
+ }
+
+ if (this.isPreviewFrame)
+ {
+ return this.metadata.ImageMetadata?.PreviewSize?.YSize ?? 0;
+ }
+
+ return this.metadata.YSize;
+ }
+ }
+
+ public JxlFrameDimensions FrameDimensions
+ {
+ get
+ {
+ int xsize = this.DefaultXSize;
+ int ysize = this.DefaultYSize;
+
+ xsize = this.frameSize.Width != 0 ? this.frameSize.Width : xsize;
+ ysize = this.frameSize.Height != 0 ? this.frameSize.Height : ysize;
+
+ if (this.dcLevel != 0)
+ {
+ xsize = JxlMath.DivCeil(xsize, 1 << (3 * (int)this.dcLevel));
+ ysize = JxlMath.DivCeil(ysize, 1 << (3 * (int)this.dcLevel));
+ }
+
+ JxlFrameDimensions frameDim = new(
+ xsize,
+ ysize,
+ (int)this.groupSizeShift,
+ this.chromaSubsampling?.MaxHShift ?? 0,
+ this.chromaSubsampling?.MaxVShift ?? 0,
+ this.encoding == JxlFrameEncoding.Modular,
+ (int)this.upsampling);
+
+ return frameDim;
+ }
+ }
+
+ public bool NeedsColorTransform => !this.saveBeforeColorTransform ||
+ this.frameType == JxlFrameType.RegularFrame ||
+ this.frameType == JxlFrameType.SkipProgressive;
+
+ ///
+ /// Gets a value indicating whether this frame is supposed to be saved for future usage by other frames.
+ ///
+ public bool CanBeReferenced => // DC frames cannot be referenced. The last frame cannot be referenced.
+ // A duration 0 frame makes little sense if it is not referenced.
+ // A non-duration 0 frame may or may not be referenced.
+ !this.isLast &&
+ this.frameType != JxlFrameType.DcFrame &&
+ (this.animationFrame?.Duration == 0 || this.saveAsReference != 0);
+
+ private void UpdateFlag(bool condition, ulong flag)
+ {
+ if (condition)
+ {
+ this.flags |= flag;
+ }
+ else
+ {
+ this.flags &= ~flag;
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ bool allDefault = false;
+ if (visitor.AllDefault(this, ref allDefault))
+ {
+ visitor.SetDefault(this);
+ return true;
+ }
+
+ if (!VisitFrameType(visitor, JxlFrameType.RegularFrame, ref this.frameType))
+ {
+ return false;
+ }
+
+ if (visitor.IsReading && this.isPreviewFrame && this.frameType != JxlFrameType.RegularFrame)
+ {
+ throw new InvalidOperationException("Only regular frame could be a preview");
+ }
+
+ // FrameEncoding
+ bool isModular = this.encoding == JxlFrameEncoding.Modular;
+ if (!visitor.Boolean(false, ref isModular))
+ {
+ return false;
+ }
+
+ this.encoding = isModular
+ ? JxlFrameEncoding.Modular
+ : JxlFrameEncoding.VarDct;
+
+ // Flags
+ if (!visitor.U64(0, ref this.flags))
+ {
+ return false;
+ }
+
+ // Color transform
+ bool xybEncoded = this.metadata?.ImageMetadata?.XybEncoded == true;
+ if (xybEncoded)
+ {
+ this.colorTransform = JxlColorTransform.Xyb;
+ }
+ else
+ {
+ bool alternate = this.colorTransform == JxlColorTransform.YCbCr;
+ if (!visitor.Boolean(false, ref alternate))
+ {
+ return false;
+ }
+
+ this.colorTransform = alternate
+ ? JxlColorTransform.YCbCr
+ : JxlColorTransform.None;
+ }
+
+ // Chroma subsampling
+ if (visitor.Conditional(this.colorTransform == JxlColorTransform.YCbCr &&
+ ((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0)))
+ {
+ if (!visitor.VisitNested(this.chromaSubsampling!))
+ {
+ return false;
+ }
+ }
+
+ int numExtraChannels = this.metadata?.ImageMetadata?.ExtraChannelCount ?? 0;
+
+ // Upsampling
+ if (visitor.Conditional((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(4),
+ JxlFieldExpressions.Value(8),
+ 1,
+ ref this.upsampling))
+ {
+ return false;
+ }
+
+ if (this.metadata != null && visitor.Conditional(numExtraChannels != 0))
+ {
+ List extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? [];
+ this.extraChannelUpsampling = new List(extraChannels.Count);
+
+ for (int i = 0; i < extraChannels.Count; i++)
+ {
+ uint dimShift = (uint)extraChannels[i].DimensionShift;
+ uint ecUpsampling = 1;
+ ecUpsampling >>= (int)dimShift;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(4),
+ JxlFieldExpressions.Value(8),
+ 1,
+ ref ecUpsampling))
+ {
+ return false;
+ }
+
+ ecUpsampling <<= (int)dimShift;
+
+ if (ecUpsampling < this.upsampling)
+ {
+ throw new InvalidOperationException("EC upsampling < color upsampling, invalid");
+ }
+
+ if (ecUpsampling > 8)
+ {
+ throw new InvalidOperationException("EC upsampling too large");
+ }
+
+ this.extraChannelUpsampling.Add(ecUpsampling);
+ }
+ }
+ else
+ {
+ this.extraChannelUpsampling.Clear();
+ }
+ }
+
+ // Modular / VarDCT specifics
+ if (visitor.Conditional(this.encoding == JxlFrameEncoding.Modular))
+ {
+ if (!visitor.Bits(2, 1, ref this.groupSizeShift))
+ {
+ return false;
+ }
+ }
+
+ if (visitor.Conditional(this.encoding == JxlFrameEncoding.VarDct &&
+ this.colorTransform == JxlColorTransform.Xyb))
+ {
+ if (!visitor.Bits(3, 3, ref this.xQmScale))
+ {
+ return false;
+ }
+
+ if (!visitor.Bits(3, 2, ref this.bQmScale))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ this.xQmScale = this.bQmScale = 2;
+ }
+
+ // Passes
+ if (visitor.Conditional(this.frameType != JxlFrameType.ReferenceOnly))
+ {
+ if (!visitor.VisitNested(this.passes))
+ {
+ return false;
+ }
+ }
+
+ // DC frame
+ if (visitor.Conditional(this.frameType == JxlFrameType.DcFrame))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ JxlFieldExpressions.Value(4),
+ 1,
+ ref this.dcLevel))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ this.dcLevel = 0;
+ }
+
+ // Custom size/origin
+ bool isPartialFrame = false;
+
+ if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame))
+ {
+ if (!visitor.Boolean(false, ref this.customSizeOrOrigin))
+ {
+ return false;
+ }
+
+ if (visitor.Conditional(this.customSizeOrOrigin))
+ {
+ JxlU32Enc enc = new(
+ JxlFieldExpressions.Bits(8),
+ JxlFieldExpressions.BitsOffset(11, 256),
+ JxlFieldExpressions.BitsOffset(14, 2304),
+ JxlFieldExpressions.BitsOffset(30, 18688));
+
+ if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive))
+ {
+ uint ux0 = JxlPackSigned.PackUnsigned(this.frameOrigin.X);
+ uint uy0 = JxlPackSigned.PackUnsigned(this.frameOrigin.Y);
+
+ if (!visitor.U32(enc, 0, ref ux0))
+ {
+ return false;
+ }
+
+ if (!visitor.U32(enc, 0, ref uy0))
+ {
+ return false;
+ }
+
+ this.frameOrigin = new Point(JxlPackSigned.UnpackSigned(ux0), JxlPackSigned.UnpackSigned(uy0));
+ }
+
+ uint frameSizeWidth = (uint)this.frameSize.Width;
+ uint frameSizeHeight = (uint)this.frameSize.Height;
+
+ if (!visitor.U32(enc, 0, ref frameSizeWidth))
+ {
+ return false;
+ }
+
+ if (!visitor.U32(enc, 0, ref frameSizeHeight))
+ {
+ return false;
+ }
+
+ if (this.customSizeOrOrigin && (this.frameSize.Width == 0 || this.frameSize.Height == 0))
+ {
+ throw new InvalidOperationException("Invalid crop dimensions for frame");
+ }
+
+ int imageXSize = this.DefaultXSize;
+ int imageYSize = this.DefaultYSize;
+
+ if (this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)
+ {
+ isPartialFrame |= this.frameOrigin.X > 0;
+ isPartialFrame |= this.frameOrigin.Y > 0;
+ isPartialFrame |= (this.frameSize.Width + this.frameOrigin.X) < imageXSize;
+ isPartialFrame |= (this.frameSize.Height + this.frameOrigin.Y) < imageYSize;
+ }
+ }
+ }
+
+ // Blending, animation, last frame
+ if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive))
+ {
+ this.blendingInfo!.ExtraChannelCount = numExtraChannels;
+ this.blendingInfo.IsPartialFrame = isPartialFrame;
+
+ if (!visitor.VisitNested(this.blendingInfo))
+ {
+ return false;
+ }
+
+ bool replaceAll = this.blendingInfo.BlendMode == JxlBlendMode.Replace;
+
+ this.extraChannelBlendingInfo = new List(numExtraChannels);
+ for (int i = 0; i < numExtraChannels; i++)
+ {
+ JxlBlendingInfo ecBlendingInfo = new()
+ {
+ IsPartialFrame = isPartialFrame,
+ ExtraChannelCount = numExtraChannels
+ };
+
+ if (!visitor.VisitNested(ecBlendingInfo))
+ {
+ return false;
+ }
+
+ this.extraChannelBlendingInfo.Add(ecBlendingInfo);
+ replaceAll &= ecBlendingInfo.BlendMode == JxlBlendMode.Replace;
+ }
+
+ if (visitor.IsReading && this.isPreviewFrame)
+ {
+ if (!replaceAll || this.customSizeOrOrigin)
+ {
+ throw new InvalidOperationException("Preview is not compatible with blending");
+ }
+ }
+
+ if (visitor.Conditional(this.metadata?.ImageMetadata?.HaveAnimation == true))
+ {
+ this.animationFrame!.CodecMetadata = this.metadata;
+
+ if (!visitor.VisitNested(this.animationFrame!))
+ {
+ return false;
+ }
+ }
+
+ if (!visitor.Boolean(true, ref this.isLast))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ this.isLast = false;
+ }
+
+ // SaveAsReference
+ if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame && !this.isLast))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref this.saveAsReference))
+ {
+ return false;
+ }
+ }
+
+ // SaveBeforeColorTransform logic
+ if (this.frameType != JxlFrameType.DcFrame)
+ {
+ if (visitor.Conditional(
+ this.CanBeReferenced &&
+ this.blendingInfo?.BlendMode == JxlBlendMode.Replace &&
+ !isPartialFrame &&
+ (this.frameType == JxlFrameType.RegularFrame ||
+ this.frameType == JxlFrameType.SkipProgressive)))
+ {
+ if (!visitor.Boolean(false, ref this.saveBeforeColorTransform))
+ {
+ return false;
+ }
+ }
+ else if (visitor.Conditional(this.frameType == JxlFrameType.ReferenceOnly))
+ {
+ if (!visitor.Boolean(true, ref this.saveBeforeColorTransform))
+ {
+ return false;
+ }
+
+ int xsize = this.customSizeOrOrigin
+ ? this.frameSize.Width
+ : this.metadata!.XSize;
+
+ int ysize = this.customSizeOrOrigin
+ ? this.frameSize.Height
+ : this.metadata!.YSize;
+
+ if (!this.saveBeforeColorTransform &&
+ (xsize < this.metadata!.XSize ||
+ ysize < this.metadata!.YSize ||
+ this.frameOrigin.X != 0 ||
+ this.frameOrigin.Y != 0))
+ {
+ throw new InvalidOperationException("Non-patch reference frame with invalid crop");
+ }
+ }
+ }
+ else
+ {
+ this.saveBeforeColorTransform = true;
+ }
+
+ if (!VisitNameString(visitor, ref this.name))
+ {
+ return false;
+ }
+
+ this.loopFilter!.IsModular = isModular;
+ if (!visitor.VisitNested(this.loopFilter!))
+ {
+ return false;
+ }
+
+ if (!visitor.BeginExtensions(ref this.extensions))
+ {
+ return false;
+ }
+
+ return visitor.EndExtensions();
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
new file mode 100644
index 0000000000..098bcb57fc
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Optional steps for postprocessing. These flags are the
+/// source of truth. Override must set/clear them rather than
+/// change their meaning. Values chosen such that typical flags
+/// are 0, encoded in only two bits.
+///
+[Flags]
+internal enum JxlFrameHeaderFlags : byte
+{
+ ///
+ /// Noise is injected into decoded output.
+ ///
+ Noise = 1,
+
+ ///
+ /// Overlay patches.
+ ///
+ Patches = 2,
+
+ ///
+ /// Overlay splines.
+ ///
+ Splines = 16,
+
+ ///
+ /// Implies skip adaptive DC smoothing.
+ ///
+ Dc = 32,
+
+ SkipAdaptiveDcSmoothing = 128,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
new file mode 100644
index 0000000000..6aa73eab26
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Defines the type of a JPEG XL frame.
+///
+internal enum JxlFrameType : byte
+{
+ ///
+ /// A regular frame. It might be a crop, and it will be blended
+ /// on a previous frame (if any) and likely displayed or blended in
+ /// future frames.
+ ///
+ RegularFrame,
+
+ ///
+ /// A DC frame. It is downsampled and only used as the DC
+ /// of a future and, possibly, preview frame. This cannot be cropped,
+ /// blended, or referenced by patches or blending modes. Frames using
+ /// DC cannot have non-default sizes.
+ ///
+ DcFrame,
+
+ ///
+ /// A PatchesSource frame. Can only be used as source frame for
+ /// taking patches. It can be cropped but can't have a non-(0, 0) x0/y0.
+ ///
+ ReferenceOnly = 2,
+
+ ///
+ /// Same as regular frame but not used for progressive rendering.
+ /// Implies no early display of DC.
+ ///
+ SkipProgressive,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
new file mode 100644
index 0000000000..87277414ac
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
@@ -0,0 +1,220 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Used for decoding to lower resolutions.
+///
+internal sealed class JxlPasses : IJxlFields
+{
+ ///
+ /// Defines the maximum amount of passes, which is 11.
+ ///
+ private const int MaxPasses = 11;
+
+ private uint numPasses;
+ private uint numDownsample;
+
+ ///
+ /// Gets or sets the number of passes.
+ ///
+ public uint NumberOfPasses
+ {
+ get => this.numPasses;
+ set => this.numPasses = value;
+ }
+
+ ///
+ /// Gets or sets the number of downsamples.
+ ///
+ public uint NumberOfDownsamples
+ {
+ get => this.numDownsample;
+ set => this.numDownsample = value;
+ }
+
+ ///
+ /// Gets the downsample values.
+ ///
+ public uint[] Downsample { get; } = new uint[MaxPasses];
+
+ ///
+ /// Gets the last pass values.
+ ///
+ public uint[] LastPass { get; } = new uint[MaxPasses];
+
+ ///
+ /// Gets the shift values.
+ ///
+ public uint[] Shift { get; } = new uint[MaxPasses];
+
+ public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift)
+ {
+ maxShift = 2;
+ minShift = 3;
+
+ for (int i = 0; ; i++)
+ {
+ for (int j = 0; j < this.numDownsample; ++j)
+ {
+ if (i == this.LastPass[j])
+ {
+ uint ds = this.Downsample[j];
+
+ if (ds == 8)
+ {
+ minShift = 3;
+ }
+
+ if (ds == 4)
+ {
+ minShift = 2;
+ }
+
+ if (ds == 2)
+ {
+ minShift = 1;
+ }
+
+ if (ds == 1)
+ {
+ minShift = 0;
+ }
+ }
+ }
+
+ if (i == this.numPasses - 1)
+ {
+ minShift = 0;
+ }
+
+ if (i == pass)
+ {
+ return;
+ }
+
+ maxShift = minShift - 1;
+ }
+ }
+
+ public uint GetDownsamplingTargetForCompletedPasses(int num)
+ {
+ if (num >= this.numPasses)
+ {
+ return 1;
+ }
+
+ uint result = 0;
+
+ for (int i = 0; i < this.numDownsample; i++)
+ {
+ if (num > this.LastPass[i])
+ {
+ result = Math.Min(result, this.Downsample[i]);
+ }
+ }
+
+ return result;
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.BitsOffset(1, 3),
+ 0,
+ ref this.numPasses))
+ {
+ return false;
+ }
+
+ if (this.numPasses > MaxPasses)
+ {
+ return false;
+ }
+
+ if (visitor.Conditional(this.numPasses != 1))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.BitsOffset(1, 3),
+ 0,
+ ref this.numDownsample))
+ {
+ return false;
+ }
+
+ if (this.numDownsample > 4)
+ {
+ return false;
+ }
+
+ if (this.numDownsample > this.numPasses)
+ {
+ throw new InvalidOperationException("Number of downsaples is greater than number of passes");
+ }
+
+ for (int i = 0; i < this.numPasses - 1; i++)
+ {
+ if (!visitor.Bits(2, 0u, ref this.Shift[i]))
+ {
+ return false;
+ }
+ }
+
+ this.Shift[this.numPasses - 1] = 0;
+
+ for (int i = 0; i < this.numDownsample; i++)
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(4),
+ JxlFieldExpressions.Value(8),
+ 1,
+ ref this.Downsample[i]))
+ {
+ return false;
+ }
+
+ if (i > 0 && this.Downsample[i] >= this.Downsample[i - 1])
+ {
+ throw new InvalidOperationException("Downsample sequence should decrease");
+ }
+ }
+
+ for (int i = 0; i < this.numDownsample; i++)
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref this.LastPass[i]))
+ {
+ return false;
+ }
+
+ if (i > 0 && this.LastPass[i] <= this.LastPass[i - 1])
+ {
+ throw new InvalidOperationException("Last pass sequence should increase");
+ }
+
+ if (this.LastPass[i] >= this.numPasses)
+ {
+ throw new InvalidOperationException("Last pass is greater than number of passes");
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
similarity index 98%
rename from src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs
rename to src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
index 90d9cf6738..b10d8818aa 100644
--- a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
@@ -3,7 +3,7 @@
using SixLabors.ImageSharp.Formats.Jxl.Fields;
-namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
///
/// Gets the Y'Cb'Cr chroma subsampling information as part