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