Browse Source

Add frame header

pull/3153/head
winscripter 2 months ago
parent
commit
16c55f343a
  1. 1
      src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
  2. 6
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
  3. 1
      src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
  4. 74
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
  5. 64
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
  6. 146
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
  7. 26
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
  8. 39
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
  9. 20
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
  10. 743
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
  11. 36
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
  12. 37
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
  13. 220
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
  14. 2
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs

1
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;

6
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];
}

1
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

74
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;
/// <summary>
/// Describes duration of frames that make up an animation.
/// </summary>
internal sealed class JxlAnimationFrame : IJxlFields
{
/// <summary>
/// See <see cref="Duration"/>.
/// </summary>
private uint duration;
/// <summary>
/// See <see cref="Timecode"/>.
/// </summary>
private uint timecode;
/// <summary>
/// Gets or sets the duration of the animation.
/// </summary>
public uint Duration
{
get => this.duration;
set => this.duration = value;
}
/// <summary>
/// Gets or sets the timecode of the animation. The
/// format is 0xHHMMSSFF.
/// </summary>
public uint Timecode
{
get => this.timecode;
set => this.timecode = value;
}
/// <summary>
/// Gets or sets the optional codec metadata.
/// </summary>
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;
}
}

64
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;
/// <summary>
/// Represents the blending mode describing how to combine
/// current frame with previously saved frame.
/// </summary>
internal enum JxlBlendMode : byte
{
/// <summary>
/// New values replace old ones.
/// <code>
/// sample = new
/// </code>
/// </summary>
Replace,
/// <summary>
/// New values add to the old ones.
/// <code>
/// sample = old + new
/// </code>
/// </summary>
Add,
/// <summary>
/// New values replace old ones if alpha&gt;0:
/// <code>
/// alpha = old + new * (1 - old)
/// </code>
/// For other channels if !alpha_associated:
/// <code>
/// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha
/// </code>
/// For other channels if alpha_associated:
/// <code>
/// sample = (1 - newAlpha) * old + new
/// </code>
/// </summary>
Blend,
/// <summary>
/// New values are added to the old ones if alpha&gt;0:
/// For the alpha channel that is used as source:
/// <code>
/// sample = old + new * (1 - old)
/// </code>
/// Otherwise:
/// <code>
/// sample = old + alpha * new
/// </code>
/// </summary>
AlphaWeightedBlend,
/// <summary>
/// New values are multiplied by old ones:
/// <code>
/// sample = old * new
/// </code>
/// </summary>
Multiply
}

146
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;
/// <summary>
/// Provides options and instructions that tell the decoder the proper
/// way to blend the current and previous frame together.
/// </summary>
internal sealed class JxlBlendingInfo : IJxlFields
{
/// <summary>
/// Initializes a new instance of the <see cref="JxlBlendingInfo"/> class.
/// </summary>
public JxlBlendingInfo() => JxlBundle.Init(this);
/// <summary>
/// Gets or sets the blending mode. See <see cref="JxlBlendMode"/>.
/// </summary>
public JxlBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets the value that indicates which extra channel
/// to use as alpha channel for blending.
/// </summary>
public uint AlphaChannel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the alpha or channel values
/// must be clamped* to the 0 through 1 range.
/// </summary>
/// <remarks>
/// Clamped - must be limited to the specified range.
/// </remarks>
public bool Clamp { get; set; }
/// <summary>
/// Gets or sets the frame ID to copy from (0 through 3).
/// </summary>
/// <remarks>
/// If <see cref="BlendMode"/> is equal to <see cref="JxlBlendMode.Replace"/>,
/// the value of this property is ignored.
/// </remarks>
public uint Source { get; set; }
/// <summary>
/// Gets or sets the total number of extra channels.
/// </summary>
public int ExtraChannelCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame is partial.
/// </summary>
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;
}
}

26
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;
/// <summary>
/// Represents the type of JPEG XL color transform.
/// </summary>
internal enum JxlColorTransform : byte
{
/// <summary>
/// Use XYB encoding
/// </summary>
Xyb,
/// <summary>
/// Encode according to the attached color profile.
/// </summary>
None,
/// <summary>
/// Encode according to the attached color profile but
/// transformed into Y'Cb'Cr.
/// </summary>
YCbCr,
}

39
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;
/// <summary>
/// Helper methods associated with JxlColorTransform.
/// </summary>
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<int> 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];
}
}
}

20
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;
/// <summary>
/// Represents the kind of frame encoding.
/// </summary>
internal enum JxlFrameEncoding : byte
{
/// <summary>
/// Use VarDCT
/// </summary>
VarDct,
/// <summary>
/// Use Modular encoding
/// </summary>
Modular
}

743
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;
/// <summary>
/// Control information for a JPEG XL frame.
/// </summary>
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<uint> extraChannelUpsampling = [];
private Point frameOrigin;
private JxlBlendingInfo? blendingInfo;
private List<JxlBlendingInfo> 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
/// <summary>
/// Gets or sets the frame encoding method (e.g., Modular or VarDCT).
/// </summary>
public JxlFrameEncoding Encoding
{
get => this.encoding;
set => this.encoding = value;
}
/// <summary>
/// Gets or sets the type of frame (e.g., RegularFrame).
/// </summary>
public JxlFrameType FrameType
{
get => this.frameType;
set => this.frameType = value;
}
/// <summary>
/// Gets or sets the frame flags.
/// </summary>
public ulong Flags
{
get => this.flags;
set => this.flags = value;
}
/// <summary>
/// Gets or sets the color transform used (e.g., XYB).
/// </summary>
public JxlColorTransform ColorTransform
{
get => this.colorTransform;
set => this.colorTransform = value;
}
/// <summary>
/// Gets or sets the chroma subsampling information.
/// </summary>
public JxlYCbCrChromaSubsampling? ChromaSubsampling
{
get => this.chromaSubsampling;
set => this.chromaSubsampling = value;
}
/// <summary>
/// Gets or sets the group size shift value.
/// </summary>
public uint GroupSizeShift
{
get => this.groupSizeShift;
set => this.groupSizeShift = value;
}
/// <summary>
/// Gets or sets the X quantization matrix scale.
/// </summary>
public uint XQmScale
{
get => this.xQmScale;
set => this.xQmScale = value;
}
/// <summary>
/// Gets or sets the B quantization matrix scale.
/// </summary>
public uint BQmScale
{
get => this.bQmScale;
set => this.bQmScale = value;
}
/// <summary>
/// Gets or sets the frame name.
/// </summary>
public string? Name
{
get => this.name;
set => this.name = value;
}
/// <summary>
/// Gets or sets a value indicating whether the frame has a custom size or origin.
/// </summary>
public bool CustomSizeOrOrigin
{
get => this.customSizeOrOrigin;
set => this.customSizeOrOrigin = value;
}
/// <summary>
/// Gets or sets the frame size.
/// </summary>
public Size FrameSize
{
get => this.frameSize;
set => this.frameSize = value;
}
/// <summary>
/// Gets or sets the upsampling factor.
/// </summary>
public uint Upsampling
{
get => this.upsampling;
set => this.upsampling = value;
}
/// <summary>
/// Gets or sets the upsampling factors for extra channels.
/// </summary>
public List<uint> ExtraChannelUpsampling
{
get => this.extraChannelUpsampling;
set => this.extraChannelUpsampling = value;
}
/// <summary>
/// Gets or sets the frame origin point.
/// </summary>
public Point FrameOrigin
{
get => this.frameOrigin;
set => this.frameOrigin = value;
}
/// <summary>
/// Gets or sets the blending information for the frame.
/// </summary>
public JxlBlendingInfo? BlendingInfo
{
get => this.blendingInfo;
set => this.blendingInfo = value;
}
/// <summary>
/// Gets or sets the blending information for extra channels.
/// </summary>
public List<JxlBlendingInfo> ExtraChannelBlendingInfo
{
get => this.extraChannelBlendingInfo;
set => this.extraChannelBlendingInfo = value;
}
/// <summary>
/// Gets the associated animation frame, if any.
/// </summary>
public JxlAnimationFrame? AnimationFrame => this.animationFrame;
/// <summary>
/// Gets or sets a value indicating whether this is the last frame.
/// </summary>
public bool IsLast
{
get => this.isLast;
set => this.isLast = value;
}
/// <summary>
/// Gets or sets the reference frame index to save.
/// </summary>
public uint SaveAsReference
{
get => this.saveAsReference;
set => this.saveAsReference = value;
}
/// <summary>
/// Gets or sets a value indicating whether to save before color transform.
/// </summary>
public bool SaveBeforeColorTransform
{
get => this.saveBeforeColorTransform;
set => this.saveBeforeColorTransform = value;
}
/// <summary>
/// Gets or sets the DC level of the frame.
/// </summary>
public uint DcLevel
{
get => this.dcLevel;
set => this.dcLevel = value;
}
/// <summary>
/// Gets or sets the codec metadata.
/// </summary>
public JxlCodecMetadata? Metadata
{
get => this.metadata;
set => this.metadata = value;
}
/// <summary>
/// Gets or sets the loop filter applied to the frame.
/// </summary>
public JxlLoopFilter? LoopFilter
{
get => this.loopFilter;
set => this.loopFilter = value;
}
/// <summary>
/// Gets or sets a value indicating whether this is a preview frame. Non-serialized.
/// </summary>
public bool IsPreviewFrame
{
get => this.isPreviewFrame;
set => this.isPreviewFrame = value;
}
/// <summary>
/// Gets or sets the number of extensions.
/// </summary>
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;
/// <summary>
/// Gets a value indicating whether this frame is supposed to be saved for future usage by other frames.
/// </summary>
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<JxlExtraChannelInfo> extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? [];
this.extraChannelUpsampling = new List<uint>(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<JxlBlendingInfo>(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();
}
}

36
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;
/// <summary>
/// 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.
/// </summary>
[Flags]
internal enum JxlFrameHeaderFlags : byte
{
/// <summary>
/// Noise is injected into decoded output.
/// </summary>
Noise = 1,
/// <summary>
/// Overlay patches.
/// </summary>
Patches = 2,
/// <summary>
/// Overlay splines.
/// </summary>
Splines = 16,
/// <summary>
/// Implies skip adaptive DC smoothing.
/// </summary>
Dc = 32,
SkipAdaptiveDcSmoothing = 128,
}

37
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;
/// <summary>
/// Defines the type of a JPEG XL frame.
/// </summary>
internal enum JxlFrameType : byte
{
/// <summary>
/// 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.
/// </summary>
RegularFrame,
/// <summary>
/// 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.
/// </summary>
DcFrame,
/// <summary>
/// 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.
/// </summary>
ReferenceOnly = 2,
/// <summary>
/// Same as regular frame but not used for progressive rendering.
/// Implies no early display of DC.
/// </summary>
SkipProgressive,
}

220
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;
/// <summary>
/// Used for decoding to lower resolutions.
/// </summary>
internal sealed class JxlPasses : IJxlFields
{
/// <summary>
/// Defines the maximum amount of passes, which is 11.
/// </summary>
private const int MaxPasses = 11;
private uint numPasses;
private uint numDownsample;
/// <summary>
/// Gets or sets the number of passes.
/// </summary>
public uint NumberOfPasses
{
get => this.numPasses;
set => this.numPasses = value;
}
/// <summary>
/// Gets or sets the number of downsamples.
/// </summary>
public uint NumberOfDownsamples
{
get => this.numDownsample;
set => this.numDownsample = value;
}
/// <summary>
/// Gets the downsample values.
/// </summary>
public uint[] Downsample { get; } = new uint[MaxPasses];
/// <summary>
/// Gets the last pass values.
/// </summary>
public uint[] LastPass { get; } = new uint[MaxPasses];
/// <summary>
/// Gets the shift values.
/// </summary>
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;
}
}

2
src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs → 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;
/// <summary>
/// Gets the Y'Cb'Cr chroma subsampling information as part
Loading…
Cancel
Save