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; + } +}