From 37766b8d1a611def002bd8f649502820a376b792 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:44:53 +0400 Subject: [PATCH] Implement field visitor for metadata --- .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 131 ++++++++- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 278 ++++++++++++++++-- .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 70 ++++- .../Formats/Jxl/IO/Metadata/JxlToneMapping.cs | 90 +++++- 4 files changed, 532 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 1ad671969e..2d53a5f380 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -1,39 +1,152 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlExtraChannelInfo : IJxlFields { - public bool AllDefault { get; set; } + private bool allDefault; + private JxlExtraChannel type; + private JxlBitDepthMetadata? bitDepth; + private int dimensionShift; + private string? name; + private bool alphaAssociated; + private InlineArray4 spotColor; + private int cfaChannel; - public JxlExtraChannel Type { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } + + public JxlExtraChannel Type + { + get => this.type; + set => this.type = value; + } - public JxlBitDepthMetadata? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth + { + get => this.bitDepth; + set => this.bitDepth = value; + } - public int DimensionShift { get; set; } + public int DimensionShift + { + get => this.dimensionShift; + set => this.dimensionShift = value; + } - public string? Name { get; set; } + public string? Name + { + get => this.name; + set => this.name = value; + } - public bool AlphaAssociated { get; set; } + public bool AlphaAssociated + { + get => this.alphaAssociated; + set => this.alphaAssociated = value; + } - public InlineArray4 SpotColor { get; set; } + public InlineArray4 SpotColor + { + get => this.spotColor; + set => this.spotColor = value; + } - public int CfaChannel { get; set; } + public int CfaChannel + { + get => this.cfaChannel; + set => this.cfaChannel = value; + } public bool Visit(JxlVisitor visitor) { if (visitor.AllDefault(this, ref this.allDefault)) { + // Overwrite all serialized fields, but not any nonserialized_*. visitor.SetDefault(this); return true; } - if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.Type)) + // General + if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.type)) + { + return false; + } + + if (!visitor.VisitNested(this.bitDepth!)) { return false; } + + if (!visitor.U32( + JxlFieldExpressions.Value(0u), + JxlFieldExpressions.Value(3u), + JxlFieldExpressions.Value(4u), + JxlFieldExpressions.BitsOffset(3u, 1u), + 0u, + ref Unsafe.As(ref this.dimensionShift))) + { + return false; + } + + if ((1u << this.dimensionShift) > 8u) + { + return false; + } + + if (!VisitNameString(visitor, ref this.name)) + { + return false; + } + + // Conditional + if (visitor.Conditional(this.type == JxlExtraChannel.Alpha)) + { + if (!visitor.Boolean(false, ref this.alphaAssociated)) + { + return false; + } + } + + if (visitor.Conditional(this.type == JxlExtraChannel.SpotColor)) + { + for (int i = 0; i < 4; i++) + { + if (!visitor.F16(0F, ref this.spotColor[i])) + { + return false; + } + } + } + + if (visitor.Conditional(this.type == JxlExtraChannel.Cfa)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1u), + JxlFieldExpressions.Bits(2u), + JxlFieldExpressions.BitsOffset(4u, 3u), + JxlFieldExpressions.BitsOffset(8u, 19u), + 1u, + ref Unsafe.As(ref this.cfaChannel))) + { + return false; + } + } + + if (this.type is JxlExtraChannel.Unknown or + >= JxlExtraChannel.Reserved0 and + <= JxlExtraChannel.Reserved7) + { + return false; + } + + return true; } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 9b8d6e220e..500fefb1c8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -8,37 +9,120 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlImageMetadata : IJxlFields { - public bool AllDefault { get; set; } + private bool allDefault; + private JxlBitDepthMetadata? bitDepth; + private bool modular16BitBufferSufficient; + private bool xybEncoded; + private JxlColorEncoding? colorEncoding; + private int orientation = 1; + private bool havePreview; + private bool haveAnimation; + private bool haveIntrinsicSize; + private JxlSizeHeader intrinsicSize = new(); + private JxlToneMapping? toneMapping; + private int extraChannelCount; + private List extraChannels = []; + private JxlPreviewHeader previewSize = new(); + private JxlAnimationHeader animation = new(); + private long extensions; + + public JxlImageMetadata() => JxlBundle.Init(this); + + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } - public JxlBitDepthMetadata? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth + { + get => this.bitDepth; + set => this.bitDepth = value; + } - public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is + public bool Modular16BitBufferSufficient + { + get => this.modular16BitBufferSufficient; + set => this.modular16BitBufferSufficient = value; + } - public bool XybEncoded { get; set; } + public bool XybEncoded + { + get => this.xybEncoded; + set => this.xybEncoded = value; + } - public JxlColorEncoding? ColorEncoding { get; set; } + public JxlColorEncoding? ColorEncoding + { + get => this.colorEncoding; + set => this.colorEncoding = value; + } - public int Orientation { get; set; } = 1; + public int Orientation + { + get => this.orientation; + set => this.orientation = value; + } - public bool HavePreview { get; set; } + public bool HavePreview + { + get => this.havePreview; + set => this.havePreview = value; + } - public bool HaveAnimation { get; set; } + public bool HaveAnimation + { + get => this.haveAnimation; + set => this.haveAnimation = value; + } - public bool HaveIntrinsicSize { get; set; } + public bool HaveIntrinsicSize + { + get => this.haveIntrinsicSize; + set => this.haveIntrinsicSize = value; + } - public JxlSizeHeader IntrinsicSize { get; set; } = new(); + public JxlSizeHeader IntrinsicSize + { + get => this.intrinsicSize; + set => this.intrinsicSize = value; + } - public JxlToneMapping? ToneMapping { get; set; } + public JxlToneMapping? ToneMapping + { + get => this.toneMapping; + set => this.toneMapping = value; + } - public int ExtraChannelCount { get; set; } + public int ExtraChannelCount + { + get => this.extraChannelCount; + set => this.extraChannelCount = value; + } - public List ExtraChannels { get; set; } = []; + public List ExtraChannels + { + get => this.extraChannels; + set => this.extraChannels = value; + } - public JxlPreviewHeader PreviewSize { get; set; } = new(); + public JxlPreviewHeader PreviewSize + { + get => this.previewSize; + set => this.previewSize = value; + } - public JxlAnimationHeader Animation { get; set; } = new(); + public JxlAnimationHeader Animation + { + get => this.animation; + set => this.animation = value; + } - public long Extensions { get; set; } + public long Extensions + { + get => this.extensions; + set => this.extensions = value; + } public bool NonserializedOnlyParseBasicInfos { get; set; } @@ -148,5 +232,165 @@ internal sealed class JxlImageMetadata : IJxlFields } } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor.SetDefault(this); + return true; + } + + // Bundle.AllDefault does not allow usage when reading because it may abort + // when a codestream has invalid values. When reading, extraFields is + // overwritten below, so AllDefault is not needed. + bool toneMappingDefault = !visitor.IsReading && JxlBundle.AllDefault(this.toneMapping!); + + bool extraFields = this.orientation != 1 || + this.havePreview || + this.haveAnimation || + this.haveIntrinsicSize || + !toneMappingDefault; + + if (!visitor.Boolean(false, ref extraFields)) + { + return false; + } + + if (visitor.Conditional(extraFields)) + { + this.orientation--; + + if (!visitor.Bits(3, 0, ref Unsafe.As(ref this.orientation))) + { + return false; + } + + this.orientation++; + + // No bounds checking is necessary because exactly 3 bits are read. + if (!visitor.Boolean(false, ref this.haveIntrinsicSize)) + { + return false; + } + + if (visitor.Conditional(this.haveIntrinsicSize)) + { + if (!visitor.VisitNested(this.intrinsicSize)) + { + return false; + } + } + + if (!visitor.Boolean(false, ref this.havePreview)) + { + return false; + } + + if (visitor.Conditional(this.havePreview)) + { + if (!visitor.VisitNested(this.previewSize)) + { + return false; + } + } + + if (!visitor.Boolean(false, ref this.haveAnimation)) + { + return false; + } + + if (visitor.Conditional(this.haveAnimation)) + { + if (!visitor.VisitNested(this.animation)) + { + return false; + } + } + } + else + { + this.orientation = 1; + this.haveIntrinsicSize = false; + this.havePreview = false; + this.haveAnimation = false; + } + + if (!visitor.VisitNested(this.bitDepth!)) + { + return false; + } + + if (!visitor.Boolean(true, ref this.modular16BitBufferSufficient)) + { + return false; + } + + this.extraChannelCount = this.extraChannels.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0u), + JxlFieldExpressions.Value(1u), + JxlFieldExpressions.BitsOffset(4u, 2u), + JxlFieldExpressions.BitsOffset(12u, 1u), + 0u, + ref Unsafe.As(ref this.extraChannelCount))) + { + return false; + } + + if (visitor.Conditional(this.extraChannelCount != 0)) + { + if (visitor.IsReading) + { + this.extraChannels.Clear(); + this.extraChannels.Capacity = this.extraChannelCount; + + for (int i = 0; i < this.extraChannelCount; i++) + { + this.extraChannels.Add(new JxlExtraChannelInfo()); + } + } + + for (int i = 0; i < this.extraChannels.Count; i++) + { + if (!visitor.VisitNested(this.extraChannels[i])) + { + return false; + } + } + } + + if (!visitor.Boolean(true, ref this.xybEncoded)) + { + return false; + } + + if (!visitor.VisitNested(this.colorEncoding!)) + { + return false; + } + + if (visitor.Conditional(extraFields)) + { + if (!visitor.VisitNested(this.toneMapping!)) + { + return false; + } + } + + // Treat as if only the fields up to extra channels exist. + if (visitor.IsReading && this.NonserializedOnlyParseBasicInfos) + { + return true; + } + + if (!visitor.BeginExtensions(ref Unsafe.As(ref this.extensions))) + { + return false; + } + + // Extensions: in chronological order of being added to the format. + return visitor.EndExtensions(); + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index 346e374148..9148e7dc77 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -11,13 +11,73 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlOpsinInverseMatrix : IJxlFields { - public InlineArray3 OpsinBiases; + private InlineArray3 opsinBiases; + private InlineArray3 quantBiases; + private bool allDefault; + private JxlMatrix3x3F inverseMatrix; - public InlineArray3 QuantBiases; + public InlineArray3 OpsinBiases + { + get => this.opsinBiases; + set => this.opsinBiases = value; + } - public bool AllDefault { get; set; } + public InlineArray3 QuantBiases + { + get => this.quantBiases; + set => this.quantBiases = value; + } - public JxlMatrix3x3F InverseMatrix { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public JxlMatrix3x3F InverseMatrix + { + get => this.inverseMatrix; + set => this.inverseMatrix = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor.SetDefault(this); + return true; + } + + JxlMatrix3x3F defaultInverse = JxlCms.DefaultInverseOpsinAbsorbanceMatrix(); + + for (int j = 0; j < 3; j++) + { + for (int i = 0; i < 3; i++) + { + if (!visitor.F16(defaultInverse[j][i], ref this.inverseMatrix[j][i])) + { + return false; + } + } + } + + for (int i = 0; i < 3; i++) + { + if (!visitor.F16(JxlCms.NegOpsinAbsorbanceBiasRgb[i], ref this.opsinBiases[i])) + { + return false; + } + } + + for (int i = 0; i < 4; i++) + { + if (!visitor.F16(DefaultQuantBias[i], ref this.quantBiases[i])) + { + return false; + } + } + + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs index 9a399dc0b0..3c3b5a498a 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs @@ -7,15 +7,93 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlToneMapping : IJxlFields { - public bool AllDefault { get; set; } + private bool allDefault; + private float intensityTarget; + private float lowerBoundIntensityLevel; + private bool relativeToMaxDisplay; + private float linearBelow; + private float minNits; - public float IntensityTarget { get; set; } + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } - public float LowerBoundIntensityLevel { get; set; } + public float IntensityTarget + { + get => this.intensityTarget; + set => this.intensityTarget = value; + } - public bool RelativeToMaxDisplay { get; set; } + public float LowerBoundIntensityLevel + { + get => this.lowerBoundIntensityLevel; + set => this.lowerBoundIntensityLevel = value; + } - public float LinearBelow { get; set; } + public bool RelativeToMaxDisplay + { + get => this.relativeToMaxDisplay; + set => this.relativeToMaxDisplay = value; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public float LinearBelow + { + get => this.linearBelow; + set => this.linearBelow = value; + } + + public float MinimumNits + { + get => this.minNits; + set => this.minNits = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.AllDefault(this, ref this.allDefault)) + { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor.SetDefault(this); + return true; + } + + if (!visitor.F16(JxlFieldExpressions.Value(JxlConstants.DefaultIntensityTarget), ref this.intensityTarget)) + { + return false; + } + + if (this.intensityTarget <= 0F) + { + return false; + } + + if (!visitor.F16(0F, ref this.minNits)) + { + return false; + } + + if (this.minNits < 0F || this.minNits > this.intensityTarget) + { + return false; + } + + if (!visitor.Boolean(false, ref this.relativeToMaxDisplay)) + { + return false; + } + + if (!visitor.F16(0F, ref this.linearBelow)) + { + return false; + } + + if (this.linearBelow < 0F || (this.relativeToMaxDisplay && this.linearBelow > 1F)) + { + return false; + } + + return true; + } }