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