From 20e3a3452a4add118c29c79f368164cbcacdcdfb Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:35:38 +0400
Subject: [PATCH] Add image bundle
---
.../Formats/Jxl/Processing/JxlImageBundle.cs | 506 ++++++++++++++++++
1 file changed, 506 insertions(+)
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs
new file mode 100644
index 000000000..48f84175b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs
@@ -0,0 +1,506 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Cms;
+using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// An image bundle.
+///
+internal sealed class JxlImageBundle
+{
+ ///
+ /// Image data for additional channels.
+ ///
+ private List extraChannels = [];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JxlImageBundle()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified image metadata.
+ ///
+ /// Initial image metadata.
+ public JxlImageBundle(JxlImageMetadata? metadata) => this.Metadata = metadata;
+
+ ///
+ /// Gets or sets the optional name of the bundle.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the blend mode. Default is Blend.
+ ///
+ public JxlBlendMode BlendMode { get; set; } = JxlBlendMode.Blend;
+
+ ///
+ /// Gets or sets a value indicating whether blending should be done. (Default: false)
+ ///
+ public bool Blend { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this a reference frame.
+ ///
+ public bool UseForNextFrame { get; set; }
+
+ ///
+ /// Gets or sets the duration for animation.
+ ///
+ public uint Duration { get; set; }
+
+ ///
+ /// Gets or sets the timecode for animation.
+ ///
+ public uint Timecode { get; set; }
+
+ ///
+ /// Gets or sets the frame origin.
+ ///
+ public Point Origin { get; set; }
+
+ ///
+ /// Gets or sets the chroma subsampling for Y'Cb'Cr images.
+ ///
+ public JxlYCbCrChromaSubsampling? ChromaSubsampling { get; set; }
+
+ ///
+ /// Gets or sets the color transform mode for this image, the default is None.
+ ///
+ public JxlColorTransform ColorTransform { get; set; } = JxlColorTransform.None;
+
+ ///
+ /// Gets or sets the JPEG data if the input image was converted to JPEG XL from a JPEG.
+ ///
+ public JxlJpegData? JpegData { get; set; }
+
+ ///
+ /// Gets a value indicating whether returns the image does or will represent quantized DCT-8 coefficients
+ /// stored in the 8x8 pixel regions.
+ ///
+ public bool IsJpeg => this.JpegData is not null;
+
+ ///
+ /// Gets or sets the number of bytes that were actually read.
+ ///
+ public long DecodedBytes { get; set; }
+
+ ///
+ /// Gets a value indicating whether the black extra channel is present.
+ ///
+ public bool ContainsBlack => this.Metadata?.FindExtraChannel(JxlExtraChannel.Black) is not null;
+
+ ///
+ /// Gets a value indicating whether the alpha extra channel is present.
+ ///
+ public bool ContainsAlpha => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha) is not null;
+
+ ///
+ /// Gets a value indicating whether the alpha channel is premultiplied.
+ ///
+ public bool IsAlphaPremultiplied => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha)?.AlphaAssociated == true;
+
+ ///
+ /// Gets a value indicating whether the color encoding specifies Gray.
+ ///
+ public bool IsGray => this.CurrentColorEncoding?.IsGray == true;
+
+ ///
+ /// Gets a value indicating whether the color encoding specifies sRGB.
+ ///
+ public bool IsSrgb => this.CurrentColorEncoding?.IsSrgb == true;
+
+ ///
+ /// Gets a value indicating whether the color encoding specifies linear sRGB.
+ ///
+ public bool IsLinearSrgb => this.CurrentColorEncoding?.IsLinearSrgb == true;
+
+ ///
+ /// Gets the current color encoding for this image.
+ ///
+ public JxlColorEncoding? CurrentColorEncoding { get; private set; }
+
+ ///
+ /// Gets the image metadata for this image bundle.
+ ///
+ public JxlImageMetadata? Metadata { get; }
+
+ ///
+ /// Gets the color data.
+ ///
+ public JxlImage3F? Color { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the color data is present and usable.
+ ///
+ public bool HasColor => this.Color?.XSize != 0;
+
+ ///
+ /// Gets the width.
+ ///
+ public int XSize
+ {
+ get
+ {
+ if (this.IsJpeg)
+ {
+ return this.JpegData!.Width;
+ }
+
+ if (this.Color?.XSize != 0)
+ {
+ return this.Color!.XSize;
+ }
+
+ return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].XSize;
+ }
+ }
+
+ ///
+ /// Gets the height.
+ ///
+ public int YSize
+ {
+ get
+ {
+ if (this.IsJpeg)
+ {
+ return this.JpegData!.Height;
+ }
+
+ if (this.Color?.YSize != 0)
+ {
+ return this.Color!.YSize;
+ }
+
+ return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].YSize;
+ }
+ }
+
+ ///
+ /// Gets the black extra channel.
+ ///
+ public JxlImageF? Black
+ {
+ get
+ {
+ if (!this.ContainsBlack || this.Metadata is null)
+ {
+ return null;
+ }
+
+ int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Black) - this.Metadata.ExtraChannelInfo.Data;
+ return this.extraChannels[ec];
+ }
+ }
+
+ ///
+ /// Gets the alpha extra channel.
+ ///
+ public JxlImageF? Alpha
+ {
+ get
+ {
+ if (!this.ContainsAlpha || this.Metadata is null)
+ {
+ return null;
+ }
+
+ int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha) - this.Metadata.ExtraChannelInfo.Data;
+ return this.extraChannels[ec];
+ }
+ }
+
+ ///
+ /// Gets the oriented X size.
+ ///
+ public int OrientedXSize => this.Metadata?.Orientation > 4 ? this.YSize : this.XSize;
+
+ ///
+ /// Gets the oriented Y size.
+ ///
+ public int OrientedYSize => this.Metadata?.Orientation > 4 ? this.XSize : this.YSize;
+
+ ///
+ /// Gets the real bit depth.
+ ///
+ public uint RealBitDepth => this.Metadata!.BitDepth!.BitsPerSample;
+
+ ///
+ /// Returns false if the width or height is 0 and any extra channel
+ /// does not match this image bundle's width or height; returns true if
+ /// the sizes are otherwise correct.
+ ///
+ ///
+ /// True if the sizes are correct. False if they aren't.
+ ///
+ public bool VerifySizes()
+ {
+ if (this.ContainsExtraChannels())
+ {
+ int xs = this.XSize;
+ int ys = this.YSize;
+
+ if (xs == 0 || ys == 0)
+ {
+ return false;
+ }
+
+ foreach (JxlImageF ec in this.extraChannels)
+ {
+ if (ec.XSize != xs || ec.YSize != ys)
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Overrides the color encoding for this image bundle.
+ ///
+ /// The new color encoding.
+ public void OverrideProfile(JxlColorEncoding encoding) => this.CurrentColorEncoding = encoding;
+
+ ///
+ /// If the color data is present, assigns it to and returns true,
+ /// otherwise returns false and assigns null.
+ ///
+ /// Output color data.
+ /// True if color isn't null.
+ public bool TryGetColor(out JxlImage3F? color)
+ {
+ color = null;
+ if (this.Color is not null)
+ {
+ color = this.Color;
+ }
+
+ return color is not null;
+ }
+
+ ///
+ /// Removes the color data, replacing it with a new Image3F with 0 as width and height.
+ ///
+ public void RemoveColor() => this.Color = new JxlImage3F();
+
+ ///
+ /// Removes all extra channels, if any.
+ ///
+ public void ClearExtraChannels() => this.extraChannels.Clear();
+
+ ///
+ /// Returns true if there is at least 1 extra channel.
+ ///
+ /// Boolean indicating if extra channels are present.
+ public bool ContainsExtraChannels() => this.extraChannels.Count > 0;
+
+ ///
+ /// Returns an enumerable for extra channels.
+ ///
+ /// Extra channels enumerable.
+ public IEnumerable EnumerateExtraChannels() => this.extraChannels;
+
+ ///
+ /// Sets the extra channels.
+ ///
+ /// The extra channels.
+ ///
+ /// True if each plane had width and height greater than 0 and sizes are correct
+ /// after changing the extra channels; false otherwise.
+ ///
+ public bool TrySetExtraChannels(List extraChannels)
+ {
+ foreach (JxlImageF plane in extraChannels)
+ {
+ if (plane.XSize == 0 || plane.YSize == 0)
+ {
+ return false;
+ }
+ }
+
+ this.extraChannels = extraChannels;
+
+ return this.VerifySizes();
+ }
+
+ ///
+ /// Attempts to set the alpha channel.
+ ///
+ /// The alpha channel to set.
+ /// True if it was set successfully; false otherwise.
+ /// Thrown if the corresponding extra channel has incorrect info.
+ public bool TrySetAlpha(JxlImageF alpha)
+ {
+ if (this.Metadata is null)
+ {
+ return false;
+ }
+
+ JxlExtraChannelInfo? eci = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha);
+
+ if (eci is null)
+ {
+ return false;
+ }
+
+ if (alpha.XSize == 0 || alpha.YSize == 0)
+ {
+ return false;
+ }
+
+ int eciIndex = this.Metadata.ExtraChannelInfo.Data;
+
+ if (eciIndex != this.extraChannels.Count)
+ {
+ throw new InvalidOperationException("The SetAlpha parameter is incorrect");
+ }
+
+ this.extraChannels.Add(alpha);
+
+ return this.VerifySizes();
+ }
+
+ ///
+ /// Ensures that the metadata of this image is valid.
+ ///
+ /// True if metadata is correct. False if it isn't.
+ /// Rare.
+ public bool VerifyMetadata()
+ {
+ if (this.CurrentColorEncoding?.Icc?.IsEmpty == true)
+ {
+ return false;
+ }
+
+ if (this.Metadata?.ColorEncoding?.IsGray != this.IsGray)
+ {
+ return false;
+ }
+
+ if (this.Metadata?.HasAlpha == true)
+ {
+ JxlImageF? img = this.Alpha;
+ if (img?.XSize == 0)
+ {
+ throw new InvalidOperationException("Alpha should not have width equal to 0");
+ }
+ }
+
+ int alphaBits = this.Metadata?.AlphaBits ?? 0;
+
+ if (alphaBits > 32)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Updates the bundle from the sepcified image.
+ ///
+ /// Color data.
+ /// Current color encoding.
+ /// True if setting the image succeeded; otherwise false.
+ public bool SetFromImage(JxlImage3F color, JxlColorEncoding current)
+ {
+ if (color.XSize == 0 || color.YSize == 0)
+ {
+ return false;
+ }
+
+ if (this.Metadata?.ColorEncoding?.IsGray == this.IsGray)
+ {
+ return false;
+ }
+
+ this.Color = color;
+ this.CurrentColorEncoding = current;
+
+ return this.VerifySizes();
+ }
+
+ ///
+ /// Shrinks this image and all of its extra channels to the specified
+ /// width and height.
+ ///
+ /// The desired width.
+ /// The desired height.
+ ///
+ /// If this bundle color data or any of the extra channels
+ /// happens to have a smaller width or height than the specified
+ /// width or height, that is considered expanding, which will immediately
+ /// return false. If this method returns true, all colors and
+ /// extra channels have successfully been shrunk.
+ ///
+ public bool ShrinkTo(int width, int height)
+ {
+ if (this.HasColor)
+ {
+ if (this.Color?.ShrinkTo(width, height) != true)
+ {
+ return false;
+ }
+ }
+
+ foreach (JxlImageF extraChannel in this.extraChannels)
+ {
+ if (!extraChannel.ShrinkTo(width, height))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Copies this image bundle to a new bundle.
+ ///
+ ///
+ /// A configuration with a memory allocator.
+ ///
+ ///
+ /// A new copy of this image bundle.
+ ///
+ ///
+ /// Thrown if some extra channels cannot be copied.
+ ///
+ public JxlImageBundle Copy(Configuration configuration)
+ {
+ JxlImageBundle copy = new(this.Metadata);
+
+ if (this.Color is not null)
+ {
+ copy.Color = new JxlImage3F(configuration, this.Color.XSize, this.Color.YSize);
+ }
+
+ copy.CurrentColorEncoding = this.CurrentColorEncoding;
+ copy.JpegData = this.JpegData;
+ copy.ColorTransform = this.ColorTransform;
+ copy.ChromaSubsampling = this.ChromaSubsampling;
+
+ foreach (JxlImageF plane in this.extraChannels)
+ {
+ JxlImageF ec = new(configuration, plane.XSize, plane.YSize);
+ if (!JxlImageOperations.CopyImage(plane, ec))
+ {
+ throw new InvalidOperationException("Cannot copy extra channel");
+ }
+
+ copy.extraChannels.Add(ec);
+ }
+
+ return copy;
+ }
+}