From e03a581fc73652ce503b5275f1e17211ec4d1df9 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Mon, 17 Aug 2026 18:31:55 +0400
Subject: [PATCH] Add JPEG XL container prototype & file type box
JxlBoxHeader contains a header for boxes in the JPEG XL container.
JxlFileTypeBox represents the ftyp box.
BinaryUtils contains helper methods to read/write primitives from/to Stream in custom endianness
---
.../Formats/Jxl/IO/BinaryUtils.Generated.cs | 321 ++++++++++++++++++
src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt | 86 +++++
.../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 132 +++++++
.../Jxl/IO/Container/JxlFileTypeBox.cs | 58 ++++
4 files changed, 597 insertions(+)
create mode 100644 src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
create mode 100644 src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
create mode 100644 src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
new file mode 100644
index 000000000..055538df7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
@@ -0,0 +1,321 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+
+///
+/// Reads primitives from streams with correct endianness.
+///
+// TODO: move this class into the IO or Common folder?
+internal static class BinaryUtils
+{
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int16 ReadInt16LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt16LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int16 ReadInt16BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt16BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt16LittleEndian(Stream stream, Int16 value)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ BinaryPrimitives.WriteInt16LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt16BigEndian(Stream stream, Int16 value)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ BinaryPrimitives.WriteInt16BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt16 ReadUInt16LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt16LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt16 ReadUInt16BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt16BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt16LittleEndian(Stream stream, UInt16 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ BinaryPrimitives.WriteUInt16LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt16BigEndian(Stream stream, UInt16 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ BinaryPrimitives.WriteUInt16BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int32 ReadInt32LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt32LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int32 ReadInt32BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt32BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt32LittleEndian(Stream stream, Int32 value)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ BinaryPrimitives.WriteInt32LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt32BigEndian(Stream stream, Int32 value)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ BinaryPrimitives.WriteInt32BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt32 ReadUInt32LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt32LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt32 ReadUInt32BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt32BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt32LittleEndian(Stream stream, UInt32 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ BinaryPrimitives.WriteUInt32LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt32BigEndian(Stream stream, UInt32 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ BinaryPrimitives.WriteUInt32BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int64 ReadInt64LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt64LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int64 ReadInt64BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt64BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt64LittleEndian(Stream stream, Int64 value)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ BinaryPrimitives.WriteInt64LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt64BigEndian(Stream stream, Int64 value)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ BinaryPrimitives.WriteInt64BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt64 ReadUInt64LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt64LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt64 ReadUInt64BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt64BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt64LittleEndian(Stream stream, UInt64 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ BinaryPrimitives.WriteUInt64LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt64BigEndian(Stream stream, UInt64 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ BinaryPrimitives.WriteUInt64BigEndian(data, value);
+ stream.Write(data);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
new file mode 100644
index 000000000..c83e9e4c4
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
@@ -0,0 +1,86 @@
+<#@ template language="C#" #>
+<#@ import namespace="System" #>
+<#@ import namespace="System.IO" #>
+<#@ import namespace="System.Collections.Generic" #>
+<#@ output extension=".Generated.cs" #>
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+<#
+ List types = [
+ typeof(short),
+ typeof(ushort),
+ typeof(int),
+ typeof(uint),
+ typeof(long),
+ typeof(ulong)
+ ];
+#>
+
+///
+/// Reads primitives from streams with correct endianness.
+///
+// TODO: move this class into the IO or Common folder?
+internal static class BinaryUtils
+{
+<#
+ foreach (Type type in types)
+ {
+#>
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static <#= type.Name #> Read<#= type.Name#>LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.Read<#= type.Name #>LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static <#= type.Name #> Read<#= type.Name#>BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.Read<#= type.Name #>BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void Write<#= type.Name #>LittleEndian(Stream stream, <#= type.Name #> value)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ BinaryPrimitives.Write<#= type.Name #>LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void Write<#= type.Name #>BigEndian(Stream stream, <#= type.Name #> value)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ BinaryPrimitives.Write<#= type.Name #>BigEndian(data, value);
+ stream.Write(data);
+ }
+<# } #>
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
new file mode 100644
index 000000000..d39f3c7e7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container;
+
+///
+/// Header for JPEG XL container format.
+///
+[StructLayout(LayoutKind.Sequential, Size = 16)]
+internal struct JxlBoxHeader
+{
+ ///
+ /// Box size in bytes.
+ ///
+ public ulong Size;
+
+ ///
+ /// Type of the box.
+ ///
+ public uint Type;
+
+ ///
+ /// True if the size field extends until the end of the file.
+ ///
+ public bool SizeExtendsTillEnd;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The size of the box.
+ /// The type of the box.
+ /// Does the box size extend till the end of the file?
+ public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd)
+ {
+ this.Size = size;
+ this.Type = type;
+ this.SizeExtendsTillEnd = sizeExtendsTillEnd;
+ }
+
+ ///
+ /// Converts a 4-character ASCII string (e.g. "jxlc") into a uint type code.
+ ///
+ /// Input type string to convert
+ /// Unsigned integer representation of the type string
+ public static uint TypeFromString(string typeString)
+ {
+ if (typeString.Length != 4)
+ {
+ throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString));
+ }
+
+ Span buffer = stackalloc byte[4];
+ _ = Encoding.ASCII.GetBytes(typeString, buffer);
+
+ return BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ }
+
+ ///
+ /// Converts a uint type code back into a 4-character ASCII string.
+ ///
+ /// Unsigned integer representation of the type string
+ /// The string representing the type code.
+ public static string TypeToString(uint typeCode)
+ {
+ Span buffer = stackalloc byte[4];
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode);
+
+ return Encoding.ASCII.GetString(buffer);
+ }
+
+ ///
+ /// Parses the JPEG XL box header.
+ ///
+ /// A stream to parse the header from.
+ /// The box header.
+ /// Thrown when the header is invalid.
+ public static JxlBoxHeader ReadHeader(Stream stream)
+ {
+ ulong size = BinaryUtils.ReadUInt32BigEndian(stream);
+ bool haveSize64 = false;
+
+ if (size == 1)
+ {
+ // When the size value is equal to 1, a new 64-bit
+ // size field follows.
+ haveSize64 = true;
+ size = BinaryUtils.ReadUInt64BigEndian(stream);
+ }
+
+ // Read the 4-byte type field.
+ uint type = BinaryUtils.ReadUInt32BigEndian(stream);
+
+ if (haveSize64)
+ {
+ // When the 64-bit largesize was read,
+ // the size cannot proceed till the end of the file.
+ if (size is 0 or 1)
+ {
+ throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file");
+ }
+
+ return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false);
+ }
+ else
+ {
+ return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0);
+ }
+ }
+
+ ///
+ /// Writes the box header to the specified stream.
+ ///
+ /// The stream to write the box header to.
+ public readonly void WriteHeader(Stream writer)
+ {
+ if (this.Size is > uint.MaxValue or 1)
+ {
+ BinaryUtils.WriteUInt32BigEndian(writer, 1); // Indicates a large size is present
+ BinaryUtils.WriteUInt64BigEndian(writer, this.Size);
+ }
+ else
+ {
+ BinaryUtils.WriteUInt32BigEndian(writer, (uint)this.Size);
+ }
+
+ BinaryUtils.WriteUInt32BigEndian(writer, this.Type);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
new file mode 100644
index 000000000..ca2adb1ca
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container;
+
+///
+/// A ftyp box payload.
+///
+internal sealed class JxlFileTypeBox(string majorBrand, uint minorVersion)
+{
+ ///
+ /// Gets or sets the primary format. Has to be "jxl " for JPEG XL.
+ ///
+ public string MajorBrand { get; set; } = majorBrand;
+
+ ///
+ /// Gets or sets the revision of the major brand.
+ ///
+ public uint MinorVersion { get; set; } = minorVersion;
+
+ ///
+ /// Gets or sets the list of other brands the file is compatible with.
+ ///
+ public List CompatibleBrands { get; set; } = [];
+
+ public int GetPayloadSize() => 8 + (this.CompatibleBrands.Count * 4);
+
+ public static JxlFileTypeBox Parse(Stream stream, ulong boxSize)
+ {
+ string majorBrand = JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream));
+ uint minorVersion = BinaryUtils.ReadUInt32BigEndian(stream);
+ boxSize -= 8;
+
+ List compatibleBrands = [];
+ for (ulong i = 0; i < boxSize; i += 4)
+ {
+ compatibleBrands.Add(JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream)));
+ }
+
+ JxlFileTypeBox ftyp = new(majorBrand, minorVersion)
+ {
+ CompatibleBrands = compatibleBrands
+ };
+
+ return ftyp;
+ }
+
+ public void WritePayload(Stream stream)
+ {
+ BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(this.MajorBrand));
+ BinaryUtils.WriteUInt32BigEndian(stream, this.MinorVersion);
+
+ foreach (string compatibleBrand in this.CompatibleBrands)
+ {
+ BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(compatibleBrand));
+ }
+ }
+}