From 8b4ff41b9ae24206032e158866eb041f02f81afe Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Sat, 15 Aug 2026 15:46:33 +0400
Subject: [PATCH] First prototype of JxlDecoderCore
- Implemented decode.cc
- Added prototype of JXL image info
- Made JxlBitReader use ReadOnlyMemory
- Made changes to ICC codecs, changing accessibility of a few methods from private to internal and exposing IccDataReader's index
- Added JxlMemoryWriter as a MemoryAllocator alternative to MemoryStream
---
.../Formats/Jxl/Fields/JxlBundle.cs | 2 +-
.../Formats/Jxl/IO/JxlMemoryWriter.cs | 82 +
src/ImageSharp/Formats/Jxl/JxlImageInfo.cs | 22 +
.../Jxl/Processing/Decoder/JxlBitReader.cs | 14 +-
.../Jxl/Processing/Decoder/JxlDecoderCore.cs | 2938 ++++++++++++++++-
.../Jxl/Processing/JxlBoxCodingMode.cs | 20 +
.../Profiles/ICC/DataReader/IccDataReader.cs | 7 +
.../Metadata/Profiles/ICC/IccReader.cs | 4 +-
8 files changed, 2988 insertions(+), 101 deletions(-)
create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs
create mode 100644 src/ImageSharp/Formats/Jxl/JxlImageInfo.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
index e6e673f9c..720293253 100644
--- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Formats.Jxl.IO;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs b/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs
new file mode 100644
index 000000000..77058802b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// A disposable writer for bytes in memory. It is highly similar to
+/// , but its buffer relies on
+/// .
+///
+internal sealed class JxlMemoryWriter(MemoryAllocator allocator) : IDisposable
+{
+ ///
+ /// The initial capacity in bytes.
+ ///
+ private const int InitialCapacity = 1024;
+
+ ///
+ /// Core buffer.
+ ///
+ private IMemoryOwner buffer = allocator.Allocate(InitialCapacity);
+
+ ///
+ /// Gets the length of the written data in bytes.
+ ///
+ public int Length { get; private set; }
+
+ ///
+ /// Gets the capacity of the buffer in bytes.
+ ///
+ public int Capacity => this.buffer.Memory.Length;
+
+ ///
+ /// Releases the underlying buffer.
+ ///
+ public void Dispose() => this.buffer.Dispose();
+
+ ///
+ /// Writes the specified bytes into the writer.
+ ///
+ /// The bytes to write.
+ public void Write(ReadOnlySpan bytes)
+ {
+ int requiredCapacity = checked(this.Length + bytes.Length);
+ this.EnsureCapacity(requiredCapacity);
+
+ bytes.CopyTo(this.buffer.Memory.Span[this.Length..]);
+ this.Length = requiredCapacity;
+ }
+
+ ///
+ /// Returns a span containing the bytes written to the writer.
+ ///
+ /// A span containing the written bytes.
+ public Span AsSpan() => this.buffer.Memory.Span[..this.Length];
+
+ ///
+ /// Returns memory containing the bytes written to the writer.
+ ///
+ /// Memory containing the written bytes.
+ public Memory AsMemory() => this.buffer.Memory[..this.Length];
+
+ private void EnsureCapacity(int requiredCapacity)
+ {
+ if (requiredCapacity <= this.Capacity)
+ {
+ return;
+ }
+
+ int newCapacity = Math.Max(requiredCapacity, checked(this.Capacity * 2));
+
+ IMemoryOwner previousBuffer = this.buffer;
+ this.buffer = allocator.Allocate(newCapacity);
+
+ previousBuffer.Memory.Span[..this.Length].CopyTo(this.buffer.Memory.Span);
+
+ previousBuffer.Dispose();
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs b/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs
new file mode 100644
index 000000000..47f5e4e95
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Image information specific to the JPEG XL format.
+///
+public class JxlImageInfo : ImageInfo
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Image size
+ /// Image metadata
+ public JxlImageInfo(Size size, ImageMetadata metadata)
+ : base(size, metadata)
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs
index cdc69838d..9dbc090ae 100644
--- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs
@@ -8,8 +8,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
///
/// Represents a bitstream reader.
///
-internal sealed class JxlBitReader(ReadOnlyMemory bytes)
+internal ref struct JxlBitReader(ReadOnlySpan bytes)
{
+ private readonly ReadOnlySpan data = bytes;
+
private ulong buffer;
private uint bufferRemainingBits;
private int pointer;
@@ -22,16 +24,14 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
///
/// Gets the total number of bits consumed.
///
- public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits);
+ public readonly long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits);
///
/// Fetches a new buffer.
///
private void RefillCore()
{
- ReadOnlySpan samplesSpan = bytes.Span;
-
- int remaining = samplesSpan.Length - this.pointer;
+ int remaining = this.data.Length - this.pointer;
if (remaining <= 0)
{
// we don't have any more data... mark an end of stream
@@ -43,7 +43,7 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
if (remaining >= 8)
{
- this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]);
+ this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(this.data[this.pointer..]);
this.bufferRemainingBits = 64u;
this.pointer += 8;
}
@@ -52,7 +52,7 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
ulong value = 0;
for (int i = 0; i < remaining; i++)
{
- value |= (ulong)samplesSpan[this.pointer + i] << (8 * i);
+ value |= (ulong)this.data[this.pointer + i] << (8 * i);
}
this.buffer = value;
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs
index fbb7a22d0..e984b36b9 100644
--- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs
@@ -1,155 +1,2911 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Common.Helpers;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
-internal sealed class JxlDecoderCore : ImageDecoderCore
+///
+/// Internal decoder for JPEG XL.
+///
+internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable
{
+ private const long NumBuffersLimit = 1 << 20;
+
+ ///
+ /// Current stage of the decoding pipeline.
+ ///
+ private JxlDecoderStage decoderStage;
+
+ ///
+ /// Status of whether or not has the signature been parsed.
+ ///
+ private bool gotSignature;
+
+ ///
+ /// Did we parse the final code stream?
+ ///
+ private bool lastCodestreamSeen;
+
+ ///
+ /// Did we parse the signature of the code stream?
+ ///
+ private bool gotCodestreamSignature;
+
+ ///
+ /// Did we parse basic JXL information?
+ ///
+ private bool gotBasicInfo;
+
+ ///
+ /// Did we parse the transform data?
+ ///
+ private bool gotTransformData;
+
+ ///
+ /// Did we parse all the codestream metadata headers?
+ ///
+ private bool gotAllHeaders;
+
+ ///
+ /// Are we decoding pixels now?
+ ///
+ private bool postHeaders;
+
+ ///
+ /// ICC profile for JPEG XL metadata, if present.
+ ///
+ private IccProfile? iccProfile;
+
+ ///
+ /// The frame index box, if present.
+ ///
+ private JxlDecoderFrameIndexBox? frameIndexBox;
+
+ ///
+ /// Did we get the preview image, or determined we cannot get it or there isn't any?
+ ///
+ private bool gotPreviewImage;
+
+ ///
+ /// Is this a preview frame?
+ ///
+ private bool previewFrame;
+
+ private long filePosition;
+
+ ///
+ /// Offset where box contents start.
+ ///
+ private long boxContentsBegin;
+
+ ///
+ /// Offset where box contents end.
+ ///
+ private long boxContentsEnd;
+
+ ///
+ /// boxContentsEnd - boxContentsBegin
+ ///
+ private long boxContentsSize;
+
+ ///
+ /// Total size of the box in bytes.
+ ///
+ private long boxSize;
+
+ ///
+ /// Size of the headers in bytes.
+ ///
+ private long headerSize;
+
+ ///
+ /// Are box contents unbounded?
+ ///
+ private bool boxContentsUnbounded;
+
+ ///
+ /// Type of the box currently being decoded.
+ ///
+ private JxlBoxType boxType;
+
+ ///
+ /// Underlying type for brob boxes.
+ ///
+ private JxlBoxType boxDecodedType;
+
+ private bool boxEvent;
+
+ ///
+ /// Should box contents be decompressed (using Brotli)?
+ ///
+ private bool decompressBoxes;
+
+ ///
+ /// Should the output buffer for the box be set?
+ ///
+ private bool boxOutBufferSet;
+
+ ///
+ /// Should the output buffer for the current box be set?
+ ///
+ private bool boxOutBufferSetCurrentBox;
+
+ ///
+ /// Output buffer for the current box.
+ ///
+ private IMemoryOwner? boxOutputBuffer;
+
+ ///
+ /// Size of the output buffer.
+ ///
+ private long boxOutBufferSize;
+
+ ///
+ /// Offset of start of box output buffer.
+ ///
+ private long boxOutBufferBegin;
+
+ ///
+ /// Current offset of start of box output buffer.
+ ///
+ private long boxOutBufferPos;
+
+ ///
+ /// Should orientation be preserved?
+ ///
+ private bool keepOrientation;
+
+ ///
+ /// Should alpha channel be unpremultiplied?
+ ///
+ private bool unpremultiplyAlpha;
+
+ private bool renderSpotcolors;
+
+ private bool coalescing;
+
+ ///
+ /// Custom intensity target.
+ ///
+ private float desiredIntensityTarget;
+
+ private int eventsWanted;
+
+ private int originalEventsWanted;
+
+ private long basicInfoSizeHint;
+
+ ///
+ /// Is container format present?
+ ///
+ private bool haveContainer;
+
+ ///
+ /// Total number of boxes.
+ ///
+ private long boxCount;
+
+ ///
+ /// The level of progressive detail in frame coding.
+ ///
+ private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Dc;
+
+ ///
+ /// Progressive detail of current frame.
+ ///
+ private JxlProgressiveDetail frameProgressiveDetail;
+
+ ///
+ /// The intended downsampling ratio for the current progression step.
+ ///
+ private long downsamplingTarget;
+
+ ///
+ /// True if the image output buffer or callback was set.
+ ///
+ private bool imageOutBufferSet;
+
+ ///
+ /// Size of the image output buffer.
+ ///
+ private long imageOutputSize;
+
+ ///
+ /// Output data for extra channels.
+ ///
+ private List extraChannelOutputs = [];
+
+ ///
+ /// Codec metadata if present.
+ ///
+ private JxlCodecMetadata? metadata;
+
+ ///
+ /// Image metadata if present.
+ ///
+ private JxlImageMetadata? imageMetadata;
+
+ ///
+ /// The image bundle.
+ ///
+ private JxlImageBundle? imageBundle;
+
+ ///
+ /// State for passes decoder.
+ ///
+ private JxlPassesDecoderState? passesState;
+
+ ///
+ /// State for frame decoder.
+ ///
+ private JxlFrameDecoder? frameDecoder;
+
+ ///
+ /// The next section.
+ ///
+ private long nextSection;
+
+ private List sectionProcessed = [];
+
+ ///
+ /// The frame header, if present.
+ ///
+ private JxlFrameHeader? frameHeader;
+
+ ///
+ /// Remaining frame size.
+ ///
+ private long remainingFrameSize;
+
+ ///
+ /// Stage of the decoding pipeline.
+ ///
+ private JxlFrameStage frameStage;
+
+ ///
+ /// Has progression for DC frames been completed?
+ ///
+ private bool dcFrameProgressionDone;
+
+ private bool isLastOfStill;
+
+ ///
+ /// Is the currently processed frame the last of the codestream?
+ ///
+ private bool isLastTotal;
+
+ ///
+ /// How many frames should be skipped?
+ ///
+ private int skipFrames;
+
+ ///
+ /// Is active frame being skipped?
+ ///
+ private bool skippingFrame;
+
+ private int internalFrames;
+
+ private int externalFrames;
+
+ ///
+ /// All frame reference.s
+ ///
+ private List frameReferences = [];
+
+ private List frameExternalToInternal = [];
+
+ private List frameRequired = [];
+
+ ///
+ /// Codestream input data is temporarily copied here.
+ ///
+ private JxlMemoryWriter? codestreamCopy;
+
+ private long codestreamUnconsumed;
+
+ ///
+ /// Position in the codestreamCopy vector.
+ ///
+ private long codestreamPos;
+
+ ///
+ /// Number of remaining bits in the codestream copy.
+ ///
+ private long codestreamBitsAhead;
+
+ ///
+ /// Stage of the box parsing pipeline.
+ ///
+ private JxlBoxStage boxStage;
+
+ ///
+ /// FTYP minor-version.
+ ///
+ /// - 0 - jxlp must be in order
+ /// - 1 - OOO jxlp allowed
+ ///
+ ///
+ private int jxlFileFormatVersion;
+
+ ///
+ /// Counter of next expected jxlp box.
+ ///
+ private int nextJxlpIndex;
+
+ ///
+ /// OOO jxlp payloads keyed by counter. Keys are: codestream bytes without
+ /// 4byte header, and is_last.
+ ///
+ private Dictionary jxlpOooBuffer = [];
+
+ private long jxlpOooBufferTotal;
+
+ private int bufferingJxlpIndex;
+
+ private bool bufferingJxlpIsLast;
+
+ ///
+ /// Decompresses box contents.
+ ///
+ private JxlBoxContentDecoder? boxContentDecoder;
+
+ ///
+ /// Decodes JPEG XL to JPEG.
+ ///
+ private JxlToJpegDecoder? jpegDecoder;
+
+ private JxlBoxContentDecoder? metadataDecoder;
+
+ ///
+ /// Raw bytes for EXIF metadata.
+ ///
+ private IMemoryOwner? exifMetadata;
+
+ ///
+ /// Raw bytes for XMP metadata.
+ ///
+ private IMemoryOwner? xmpMetadata;
+
+ ///
+ /// State of EXIF storage. 0 - not stored,
+ /// 1 - currently stored, 2 - finished.
+ ///
+ private int storeExif;
+
+ ///
+ /// State of XMP storage. 0 - not stored,
+ /// 1 - currently stored, 2 - finished.
+ ///
+ private int storeXmp;
+
+ ///
+ /// Position in the output buffer for JPEG
+ /// reconstruction.
+ ///
+ private long reconstructionOutputBufferPos;
+
+ ///
+ /// EXIF size for JPEG reconstruction.
+ ///
+ private long reconstructionExifSize;
+
+ ///
+ /// XMP size for JPEG reconstruction.
+ ///
+ private long reconstructionXmpSize;
+
+ ///
+ /// Stage of reconstruction pipeline.
+ ///
+ private JpegReconstructionStage reconstructionOutputJpeg;
+
+ ///
+ /// Next input data.
+ ///
+ private IMemoryOwner? nextInput;
+
+ private long availableInput;
+
+ private bool inputClosed;
+
+ ///
+ /// Output image buffer.
+ ///
+ private Stream? imageOutBuffer;
+
+ ///
+ /// Callback to initialize image output.
+ ///
+ private JxlImageOutputInitializerCallback? imageOutputInitCallback;
+
+ ///
+ /// Callback to run image output.
+ ///
+ private JxlImageOutputRunCallback? imageOutputRunCallback;
+
+ ///
+ /// Callback to dispose image output.
+ ///
+ private JxlImageOutputDestroyCallback? imageOutputDestroyCallback;
+
+ ///
+ /// Bit depth for image output.
+ ///
+ private JxlBitDepth imageOutputBitDepth = new();
+
+ public JxlDecoderCore(DecoderOptions options)
+ : base(options)
+ => this.Reset();
+
+ public long SizeHintBasicInfo => this.gotBasicInfo ? 0 : this.basicInfoSizeHint;
+
+ ///
+ /// Gets or sets a value indicating whether orientation should be kept.
+ ///
+ public bool KeepOrientation
+ {
+ get => this.keepOrientation;
+ set
+ {
+ this.BeforeUpdateState(nameof(this.KeepOrientation));
+ this.keepOrientation = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to unpremultiply RGB values
+ /// by the alpha channel.
+ ///
+ public bool UnpremultiplyAlpha
+ {
+ get => this.unpremultiplyAlpha;
+ set
+ {
+ this.BeforeUpdateState(nameof(this.UnpremultiplyAlpha));
+ this.unpremultiplyAlpha = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether spotcolors (special inks used in printing)
+ /// are rendered in the output.
+ ///
+ public bool RenderSpotcolors
+ {
+ get => this.renderSpotcolors;
+ set
+ {
+ this.BeforeUpdateState(nameof(this.RenderSpotcolors));
+ this.renderSpotcolors = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether multiple frames (especially zero-duration frames)
+ /// have to be merged into a single image.
+ ///
+ public bool Coalescing
+ {
+ get => this.coalescing;
+ set
+ {
+ this.BeforeUpdateState(nameof(this.Coalescing));
+ this.coalescing = value;
+ }
+ }
+
+ ///
+ /// Gets the dimensions of the current image buffer.
+ ///
+ public Size CurrentDimensions
+ {
+ get
+ {
+ int width;
+ int height;
+
+ if (this.frameHeader?.IsPreviewFrame == true)
+ {
+ width = this.metadata!.GetOrientedPreviewXSize(this.keepOrientation);
+ height = this.metadata!.GetOrientedPreviewYSize(this.keepOrientation);
+ }
+ else
+ {
+ width = this.metadata!.GetOrientedXSize(this.keepOrientation);
+ height = this.metadata!.GetOrientedYSize(this.keepOrientation);
+
+ if (!this.coalescing)
+ {
+ JxlFrameDimensions dim = this.frameHeader!.FrameDimensions;
+
+ width = dim.XSizeUpsampled;
+ height = dim.YSizeUpsampled;
+
+ if (!this.keepOrientation && this.metadata.ImageMetadata!.Orientation > 4)
+ {
+ RuntimeUtility.Swap(ref width, ref height);
+ }
+ }
+ }
+
+ return new Size(width, height);
+ }
+ }
+
+ ///
+ /// Stage of the decoder pipeline.
+ ///
+ private enum JxlDecoderStage : byte
+ {
+ ///
+ /// Initialized but hasn't decoded yet.
+ ///
+ Initialized,
+
+ ///
+ /// Decoding right now.
+ ///
+ Started,
+
+ ///
+ /// Code stream done, but other boxes could still occur.
+ ///
+ CodeStreamFinished,
+
+ ///
+ /// Decoding failed and the decoder is no longer usable.
+ ///
+ Error
+ }
+
+ ///
+ /// Identifies the signature of the JPEG XL file.
+ ///
+ private enum JxlSignature : byte
+ {
+ ///
+ /// Error status indicating not enough bytes to detect the signature.
+ ///
+ NotEnoughBytes,
+
+ ///
+ /// A JPEG XL code stream.
+ ///
+ CodeStream,
+
+ ///
+ /// The signature is invalid.
+ ///
+ Invalid,
+
+ ///
+ /// Container format.
+ ///
+ Container
+ }
+
+ ///
+ /// Represents a data type.
+ ///
+ private enum JxlDataType : byte
+ {
+ ///
+ ///
+ ///
+ UInt8,
+
+ ///
+ ///
+ ///
+ UInt16,
+
+ ///
+ ///
+ ///
+ Float,
+
+ ///
+ ///
+ ///
+ Float16
+ }
+
+ ///
+ /// Frame stage for this decoder.
+ ///
+ private enum JxlFrameStage : byte
+ {
+ ///
+ /// Frame header should be parsed.
+ ///
+ Header,
+
+ ///
+ /// TOC should be parsed.
+ ///
+ Toc,
+
+ ///
+ /// Full pixels should be parsed.
+ ///
+ Full
+ }
+
+ ///
+ /// Stage of the box parsing pipeline.
+ ///
+ private enum JxlBoxStage : byte
+ {
+ ///
+ /// Box header of the next box.
+ ///
+ Header,
+
+ ///
+ /// File type box.
+ ///
+ Ftyp,
+
+ ///
+ /// Box with skipped contents.
+ ///
+ Skip,
+
+ ///
+ /// Code stream boxes.
+ ///
+ CodeStream,
+
+ ///
+ /// Extra header of partial code stream box.
+ ///
+ PartialCodeStream,
+
+ ///
+ /// Out-of-order jxlp box payload.
+ ///
+ BufferingJxlp,
+
+ ///
+ /// Jpeg reconstruction box.
+ ///
+ JpegReconstruction
+ }
+
+ ///
+ /// Reconstruction stage for JPEG images.
+ ///
+ private enum JpegReconstructionStage : byte
+ {
+ ///
+ /// Don't output anything.
+ ///
+ None,
+
+ ///
+ /// Set metadata to the JPEG data.
+ ///
+ SetMetadata,
+
+ ///
+ /// Outputting the JPEG bytes.
+ ///
+ Output
+ }
+
+ ///
+ /// A single frame index box entry. See .
+ ///
+ private struct JxlDecoderFrameIndexBoxEntry
+ {
+ ///
+ /// Offset of start byte of this frame compared to start
+ /// byte of previous frame.
+ ///
+ public long Offset;
+
+ ///
+ /// Duration in ticks between the start of this frame and the start of the next frame.
+ ///
+ public int DurationInTicks;
+
+ ///
+ /// Amount of frames.
+ ///
+ public int AmountOfFrames;
+ }
+
+ // This is a class not a struct. This is so we can
+ // assign its values from an array access. Like this:
+ // this.frameReferences[(int)internalIndex].Reference = ...
+ // where this.frameReferences = JxlFrameReference[].
+ private sealed class JxlFrameReference(int reference, int savedAs)
+ {
+ public int Reference = reference;
+ public int SavedAs = savedAs;
+ }
+
+ ///
+ /// A frame index box.
+ ///
+ private sealed class JxlDecoderFrameIndexBox
+ {
+ ///
+ /// Gets or sets all entries within this frame index box.
+ ///
+ public List Entries { get; set; } = [];
+
+ ///
+ /// Gets the number of entries.
+ ///
+ public int Count => this.Entries.Count;
+
+ ///
+ /// Gets or sets the numerator. (Default: 1)
+ ///
+ public int Numerator { get; set; } = 1;
+
+ ///
+ /// Gets or sets the denominator. (Default: 1000)
+ ///
+ public int Denominator { get; set; } = 1000;
+
+ ///
+ /// Adds a new frame.
+ ///
+ /// Offset to first byte.
+ /// Duration in ticks.
+ /// Amount of frames.
+ public void AddFrame(long offset, int ticks, int frames) => this.Entries.Add(new JxlDecoderFrameIndexBoxEntry()
+ {
+ Offset = offset,
+ AmountOfFrames = frames,
+ DurationInTicks = ticks
+ });
+ }
+
+ private sealed record JxlExtraChannelOutput(JxlPixelFormat Format, object? Buffer, long BufferSize);
+
+ private sealed record JxlOooEntry(byte[] CodestreamBytes, bool IsLast);
+
+ public void Dispose()
+ {
+ // RewindDecodingState resets everything,
+ // including disposal of streams.
+ this.RewindDecodingState();
+
+ // Streams like input and output streams may
+ // be unmanaged.
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Ensures that the coordinates are not out of bounds.
+ ///
+ /// First coordinate
+ /// Second coordinate
+ /// Image width
+ /// Boolean indicating whether the coordinates are out of bounds
+ private static bool IsOutOfBounds(int a, int b, int size)
+ {
+ long position = a + b;
+
+ return position > size || position < a;
+ }
+
+ private static int InitialBasicInfoSizeHint()
+ {
+ const int containerHeaderSize = 48;
+ const int maxCodestreamBasicInfoSize = 50;
+ return containerHeaderSize + maxCodestreamBasicInfoSize;
+ }
+
+ private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position)
+ {
+ if (position >= length)
+ {
+ return JxlSignature.NotEnoughBytes;
+ }
+
+ buffer = buffer[position..];
+ length -= position;
+
+ // 0xFF 0x0A represents a codestream
+ if (length >= 1 && buffer[0] == 0xFF)
+ {
+ if (length < 2)
+ {
+ // We need at least two bytes for a valid codestream signature
+ return JxlSignature.NotEnoughBytes;
+ }
+ else if (buffer[1] == CodestreamMarker)
+ {
+ position += 2;
+ return JxlSignature.CodeStream;
+ }
+ else
+ {
+ return JxlSignature.Invalid;
+ }
+ }
+
+ // Container?
+ if (length >= 1 && buffer[0] == 0)
+ {
+ if (length < SignatureBox.Length)
+ {
+ return JxlSignature.NotEnoughBytes;
+ }
+ else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox))
+ {
+ position += SignatureBox.Length;
+ return JxlSignature.Container;
+ }
+ else
+ {
+ return JxlSignature.Invalid;
+ }
+ }
+
+ // Signature is invalid
+ return JxlSignature.Invalid;
+ }
+
+ private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length)
+ {
+ int position = 0;
+ return DetectSignature(buffer, length, ref position);
+ }
+
+ private static int BitsPerChannel(JxlDataType dataType)
+ => dataType switch
+ {
+ JxlDataType.UInt8 => 8,
+ JxlDataType.UInt16 or JxlDataType.Float16 => 16,
+ JxlDataType.Float => 32,
+ _ => 0
+ };
+
+ private static uint GetBitDepth(JxlBitDepth bitDepth, JxlImageMetadata metadata, JxlPixelFormat pixelFormat)
+ {
+ if (bitDepth.Type == JxlBitDepthType.FromPixelFormat)
+ {
+ return BitsPerChannel(pixelFormat.DataType);
+ }
+ else if (bitDepth.Type == JxlBitDepthType.FromCodeStream)
+ {
+ return metadata.BitDepth!.BitsPerSample;
+ }
+ else if (bitDepth.Type == JxlBitDepthType.Custom)
+ {
+ return bitDepth.BitsPerSample;
+ }
+
+ return 0;
+ }
+
+ private List GetFrameDependencies(int index, Span references)
+ {
+ DebugGuard.MustBeLessThan(index, references.Length, nameof(index));
+
+ const int storageNum = 8;
+
+ List result = [];
+ int invalid = references.Length;
+ List[] storage = new List[storageNum];
+
+ for (int s = 0; s < storageNum; s++)
+ {
+ storage[s] = new List(references.Length);
+ int mask = 1 << s;
+ int id = invalid;
+
+ for (int i = 0; i < references.Length; i++)
+ {
+ if ((references[i].SavedAs & mask) != 0)
+ {
+ id = i;
+ }
+
+ storage[s][i] = id;
+ }
+ }
+
+ Span seen = stackalloc byte[index + 1];
+ seen.Clear(); // All values are explicitly cleared in reference source
+
+ Stack stack = [];
+ stack.Push(index);
+ seen[index] = 1;
+
+ for (int s = 0; s < storageNum; s++)
+ {
+ int frameRef = storage[s][index];
+
+ if (frameRef == invalid)
+ {
+ continue;
+ }
+
+ if (seen[frameRef] != 0)
+ {
+ continue;
+ }
+
+ stack.Push(frameRef);
+ seen[frameRef] = 1;
+ result.Add(frameRef);
+ }
+
+ while (stack.Count > 0)
+ {
+ int frameIndex = stack.Pop();
+ if (frameIndex == 0)
+ {
+ continue;
+ }
+
+ for (int s = 0; s < storageNum; s++)
+ {
+ int mask = 1 << s;
+
+ if ((references[frameIndex].Reference & mask) == 0)
+ {
+ continue;
+ }
+
+ int frameRef = storage[s][frameIndex - 1];
+ if (frameRef == invalid)
+ {
+ continue;
+ }
+
+ if (seen[frameRef] != 0)
+ {
+ continue;
+ }
+
+ stack.Push(frameRef);
+ seen[frameRef] = 1;
+ result.Add(frameRef);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Checks if the buffer of specified length can be added.
+ ///
+ /// Length of the desired buffer.
+ ///
+ /// True if buffers with such lengths can be added; false if they
+ /// exceed the size limit.
+ ///
+ public bool CanAddBuffer(long length)
+ {
+ const long bufferLimit = 1 << 48;
+ return length < bufferLimit &&
+ (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.Memory.Length ?? 0)) < bufferLimit;
+ }
+
+ public bool TryInjectNextBufferedJxlpBox()
+ {
+ if (!this.jxlpOooBuffer.TryGetValue(this.nextJxlpIndex, out JxlOooEntry? value))
+ {
+ return false;
+ }
+
+ if (value == this.jxlpOooBuffer.Last().Value)
+ {
+ return true;
+ }
+
+ value.Deconstruct(out byte[] data, out bool isLast);
+ int length = data.Length;
+
+ this.codestreamCopy!.Write(data);
+
+ if (isLast)
+ {
+ this.lastCodestreamSeen = true;
+ }
+
+ _ = this.jxlpOooBuffer.Remove(this.nextJxlpIndex++);
+
+ this.jxlpOooBufferTotal -= length;
+
+ return true;
+ }
+
+ ///
+ /// Returns true if the jbrd box needs exif or xmp.
+ ///
+ /// JBRD needs more boxes - true, otherwise false.
+ public bool JbrdNeedsMoreBoxes() =>
+ (this.storeExif < 2 && this.reconstructionExifSize > 0)
+ || (this.storeXmp < 2 && this.reconstructionXmpSize > 0);
+
+ ///
+ /// Moves the input data forward by size bytes.
+ ///
+ /// Number of bytes to advance.
+ /// Thrown if advancing out of bounds.
+ public void AdvanceInput(long size)
+ {
+ if (this.availableInput < size)
+ {
+ throw new InvalidOperationException("Attempting to advance out of bounds");
+ }
+
+ this.nextInput += size;
+ this.filePosition += size;
+ this.availableInput -= size;
+ }
+
+ ///
+ /// Returns number of available bytes in the code stream.
+ ///
+ ///
+ /// Number of available code stream bytes.
+ ///
+ public long AvailableCodeStream()
+ {
+ long avail = this.availableInput;
+
+ if (!this.boxContentsUnbounded)
+ {
+ avail = Math.Min(avail, this.boxContentsEnd - this.filePosition);
+ }
+
+ return avail;
+ }
+
+ ///
+ /// Ensures that the copy of the code stream is present.
+ ///
+ ///
+ /// Thrown if the copy is missing or null.
+ ///
+ private void EnsureCodeStreamCopy()
+ {
+ if (this.codestreamCopy is null)
+ {
+ throw new InvalidOperationException("Copy of the code stream is missing");
+ }
+ }
+
+ ///
+ /// Moves forward by 'size' bytes in the code stream.
+ ///
+ /// Number of bytes to advance.
+ public void AdvanceCodeStream(long size)
+ {
+ this.EnsureCodeStreamCopy();
+ long avail = this.AvailableCodeStream();
+
+ if (this.codestreamCopy!.Length == 0)
+ {
+ if (size <= avail)
+ {
+ // We have >= size bytes available, so
+ // advancing won't be out of bounds.
+ this.AdvanceInput(size);
+ }
+ else
+ {
+ // We have a limited amount of bytes for the
+ // code stream, and advancing by size would be
+ // out of bounds. So limit the value.
+ this.codestreamPos = size - avail;
+ this.AdvanceInput(avail);
+ }
+ }
+ else
+ {
+ this.codestreamPos += size;
+ if (this.codestreamPos + this.codestreamUnconsumed >= this.codestreamCopy.Length)
+ {
+ long advance = Math.Min(
+ this.codestreamUnconsumed,
+ this.codestreamUnconsumed + this.codestreamPos - this.codestreamCopy.Length);
+
+ this.AdvanceInput(advance);
+
+ this.codestreamPos -= Math.Min(this.codestreamPos, this.codestreamCopy.Length);
+ this.codestreamUnconsumed = 0;
+
+ // Now we want to clear the code stream copy...
+ this.codestreamCopy.Dispose();
+ this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator);
+ }
+ }
+ }
+
+ ///
+ /// Attempts to expand the buffer.
+ ///
+ /// Status of requesting more input.
+ public bool TryRequestMoreInput()
+ {
+ this.EnsureCodeStreamCopy();
+
+ if (this.codestreamCopy!.Length > 0)
+ {
+ long avail = this.AvailableCodeStream();
+
+ if (!this.CanAddBuffer(avail))
+ {
+ return false;
+ }
+
+ this.codestreamCopy.Write(this.nextInput!.Memory.Span[..(int)avail]);
+
+ this.AdvanceInput(avail);
+ }
+ else
+ {
+ this.AdvanceInput(this.codestreamUnconsumed);
+ this.codestreamUnconsumed = 0;
+ }
+
+ return true;
+ }
+
+ public Memory? TryGetCodestreamInput()
+ {
+ if (this.codestreamCopy is null)
+ {
+ return null;
+ }
+
+ if (this.codestreamCopy.Length == 0 && this.codestreamPos > 0)
+ {
+ long avail = this.AvailableCodeStream();
+ long skip = Math.Min(this.codestreamPos, avail);
+ this.AdvanceInput(skip);
+ this.codestreamPos -= skip;
+
+ if (this.codestreamPos > 0)
+ {
+ _ = this.TryRequestMoreInput();
+ return null;
+ }
+ }
+
+ if (this.codestreamPos > this.codestreamCopy.Length)
+ {
+ throw new InvalidOperationException("Codestream position > length of codestream copy");
+ }
+
+ if (this.codestreamUnconsumed > this.codestreamCopy.Length)
+ {
+ throw new InvalidOperationException("Codestream unconsumed > length of codestream copy");
+ }
+
+ long availCodestream = this.AvailableCodeStream();
+
+ if (this.codestreamCopy.Length == 0)
+ {
+ if (availCodestream == 0)
+ {
+ _ = this.TryRequestMoreInput();
+ return null;
+ }
+
+ return this.nextInput!.Memory[..(int)availCodestream];
+ }
+ else
+ {
+ if (!this.CanAddBuffer(availCodestream))
+ {
+ return null;
+ }
+
+ this.codestreamCopy.Write(this.nextInput!.Memory.Span.Slice((int)this.codestreamUnconsumed, (int)(availCodestream - this.codestreamUnconsumed)));
+
+ this.codestreamUnconsumed = availCodestream;
+
+ return this.codestreamCopy.AsMemory();
+ }
+ }
+
+ ///
+ /// Returns true if the decoder can continue using code stream input.
+ ///
+ /// True if the decoder can use code stream input. Otherwise false.
+ public bool CanUseMoreCodestreamInput() => this.decoderStage != JxlDecoderStage.CodeStreamFinished;
+
+ ///
+ /// Checks if width * height can be represented safely as a
+ /// positive integer after rounding the width up to the next
+ /// multiple of 32.
+ ///
+ /// Input width.
+ /// Input height.
+ ///
+ /// Boolean indicating whether the padded image dimensions fit
+ /// within a signed 32-bit integer when calculating the total
+ /// pixel count.
+ ///
+ ///
+ /// Negative values aren't rejected, but will produce incorrect
+ /// results. This method is meant to be used with positive values only.
+ ///
+ public static bool CheckSizeLimit(int width, int height)
+ {
+ if (width == 0 || height == 0)
+ {
+ return true;
+ }
+
+ int paddedWidth = JxlMath.DivCeil(width, 32) * 32;
+
+ if (paddedWidth < width)
+ {
+ // Overflow
+ return false;
+ }
+
+ int pixelCount = paddedWidth * height;
+
+ if (pixelCount / paddedWidth != height)
+ {
+ // Overflow
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Resets the decoder state to its default values, and,
+ /// additionally, releases memory used by buffers and replaces
+ /// them with new fresh copies.
+ ///
+ public void RewindDecodingState()
+ {
+ this.decoderStage = JxlDecoderStage.Initialized;
+
+ this.gotSignature = false;
+ this.lastCodestreamSeen = false;
+ this.gotCodestreamSignature = false;
+ this.gotBasicInfo = false;
+ this.gotTransformData = false;
+ this.gotAllHeaders = false;
+ this.postHeaders = false;
+
+ this.iccProfile = null;
+
+ this.gotPreviewImage = false;
+ this.previewFrame = false;
+ this.filePosition = 0;
+
+ this.boxContentsBegin = 0;
+ this.boxContentsEnd = 0;
+ this.boxContentsSize = 0;
+ this.boxSize = 0;
+ this.headerSize = 0;
+ this.boxContentsUnbounded = false;
+
+ this.boxType = null;
+ this.boxDecodedType = null;
+
+ this.boxEvent = false;
+ this.boxStage = JxlBoxStage.Header;
+
+ this.jxlFileFormatVersion = 0;
+ this.nextJxlpIndex = 0;
+ this.jxlpOooBuffer.Clear();
+ this.jxlpOooBufferTotal = 0;
+ this.bufferingJxlpIndex = 0;
+ this.bufferingJxlpIsLast = false;
+
+ this.boxOutBufferSet = false;
+ this.boxOutBufferSetCurrentBox = false;
+ this.boxOutputBuffer?.Dispose();
+ this.boxOutputBuffer = null;
+ this.boxOutBufferSize = 0;
+ this.boxOutBufferBegin = 0;
+ this.boxOutBufferPos = 0;
+
+ this.exifMetadata?.Dispose();
+ this.exifMetadata = null;
+ this.xmpMetadata?.Dispose();
+ this.xmpMetadata = null;
+ this.storeExif = 0;
+ this.storeXmp = 0;
+
+ this.reconstructionOutputBufferPos = 0;
+ this.reconstructionExifSize = 0;
+ this.reconstructionXmpSize = 0;
+ this.reconstructionOutputJpeg = JpegReconstructionStage.None;
+
+ this.eventsWanted = this.originalEventsWanted;
+ this.basicInfoSizeHint = InitialBasicInfoSizeHint();
+ this.haveContainer = false;
+ this.boxCount = 0;
+ this.downsamplingTarget = 8;
+
+ this.imageOutBufferSet = false;
+ this.imageOutBuffer?.Dispose();
+ this.imageOutBuffer = null;
+ this.imageOutputInitCallback = null;
+ this.imageOutputRunCallback = null;
+ this.imageOutputDestroyCallback = null;
+ this.imageOutputSize = 0;
+
+ this.imageOutputBitDepth = new()
+ {
+ Type = JxlBitDepthType.FromPixelFormat
+ };
+
+ this.extraChannelOutputs.Clear();
+
+ this.nextInput?.Dispose();
+ this.nextInput = null;
+
+ this.availableInput = 0;
+ this.inputClosed = false;
+
+ this.passesState?.Reset();
+ this.frameDecoder?.Reset();
+ this.nextSection = 0;
+ this.sectionProcessed.Clear();
+
+ this.imageBundle.Reset();
+ this.metadata = new JxlCodecMetadata();
+ this.imageMetadata = this.metadata.ImageMetadata;
+
+ this.frameHeader = new()
+ {
+ Metadata = this.metadata
+ };
+
+ this.codestreamCopy?.Dispose();
+ this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator);
+ this.codestreamUnconsumed = 0;
+ this.codestreamPos = 0;
+ this.codestreamBitsAhead = 0;
+
+ this.frameStage = JxlFrameStage.Header;
+ this.remainingFrameSize = 0;
+ this.isLastOfStill = false;
+ this.isLastTotal = false;
+ this.skipFrames = 0;
+ this.skippingFrame = false;
+ this.internalFrames = 0;
+ this.externalFrames = 0;
+ }
+
+ ///
+ /// Resets the decoder to its default values.
+ ///
+ public void Reset()
+ {
+ this.RewindDecodingState();
+
+ this.keepOrientation = false;
+ this.unpremultiplyAlpha = false;
+ this.renderSpotcolors = true;
+ this.coalescing = true;
+ this.desiredIntensityTarget = 0f;
+ this.originalEventsWanted = 0;
+ this.eventsWanted = 0;
+
+ this.frameReferences.Clear();
+ this.frameExternalToInternal.Clear();
+ this.frameRequired.Clear();
+
+ this.decompressBoxes = false;
+ }
+
///
- /// Identifies the signature of the JPEG XL file.
+ /// Returns the code stream as a Span.
///
- private enum JxlSignature : byte
+ /// A Span representing the code stream.
+ /// Thrown if the code stream cannot be retrieved.
+ private Span GetCodeStreamSpan()
{
- ///
- /// Error status indicating not enough bytes to detect the signature.
- ///
- NotEnoughBytes,
+ Memory codestreamInput = this.TryGetCodestreamInput()
+ ?? throw new InvalidOperationException("Cannot retrieve codestream input");
- ///
- /// A JPEG XL code stream.
- ///
- CodeStream,
+ Span span = codestreamInput.Span;
- ///
- /// The signature is invalid.
- ///
- Invalid,
+ return span;
+ }
+
+ ///
+ /// Skips frames without decoding them.
+ ///
+ /// Number of frames to skip.
+ public void SkipFrames(int amount)
+ {
+ this.skipFrames += amount;
+ this.frameRequired.Clear();
+
+ int nextFrame = this.externalFrames + this.skipFrames;
+
+ if (nextFrame < this.frameExternalToInternal.Count)
+ {
+ int internalIndex = this.frameExternalToInternal[nextFrame];
+ if (internalIndex < this.frameReferences.Count)
+ {
+ List deps = this.GetFrameDependencies(internalIndex, CollectionsMarshal.AsSpan(this.frameReferences));
+ this.ResizeFrameRequired(internalIndex + 1);
+
+ foreach (int index in deps)
+ {
+ if (index < this.frameRequired.Count)
+ {
+ this.frameRequired[index] = 1;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Ensures that frameRequired's count reaches .
+ ///
+ /// Max. number of items that frameRequired must have.
+ private void ResizeFrameRequired(int upperBound)
+ {
+ while (this.frameRequired.Count < upperBound)
+ {
+ this.frameRequired.Add(0);
+ }
+ }
+
+ ///
+ /// Skips the current frame without having to decode it.
+ ///
+ ///
+ /// Thrown if the frame cannot be skipped.
+ ///
+ public void SkipCurrentFrame()
+ {
+ if (this.frameStage == JxlFrameStage.Full)
+ {
+ throw new InvalidOperationException("The decoder is ready to parse the frame, so the frame cannot be skipped");
+ }
+
+ this.frameStage = JxlFrameStage.Header;
+ this.AdvanceCodeStream(this.remainingFrameSize);
+
+ if (this.isLastOfStill)
+ {
+ this.imageOutBufferSet = false;
+ }
+ }
+
+ ///
+ /// Ensures that the decoder state doesn't change while
+ /// it's busy decoding an image.
+ ///
+ /// Name of the parameter that was changed/
+ /// Thrown if the state doesn't match initialization.
+ private void BeforeUpdateState(string propertyName)
+ {
+ if (this.decoderStage != JxlDecoderStage.Initialized)
+ {
+ throw new InvalidOperationException("The decoder is already processing the image, so " + propertyName + " cannot be changed");
+ }
+ }
+
+ ///
+ /// Reads a single bundle into .
+ ///
+ /// Type of the bundle to read.
+ /// Bundle binary data.
+ /// Bit reader to continue from.
+ /// The bundle to parse.
+ /// Status of parsing the bundle.
+ private bool ReadBundle(Span data, JxlBitReader br, T bundle)
+ where T : IJxlFields
+ {
+ JxlBitReader reader = new(data);
+ reader.SkipBits64((ulong)br.TotalBitsConsumed);
+
+ bool canRead = JxlBundle.CanRead(reader, bundle);
+
+ if (!canRead)
+ {
+ return this.TryRequestMoreInput();
+ }
+
+ if (!JxlBundle.Read(reader, bundle))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Reads all basic metadata and headers.
+ ///
+ /// Status of the parsing.
+ /// Thrown if the data is incorrect.
+ /// Thrown if the data is malformed.
+ public bool ReadBasicInfo()
+ {
+ if (!this.gotCodestreamSignature)
+ {
+ Span span = this.GetCodeStreamSpan();
+
+ if (span.Length < 2)
+ {
+ return this.TryRequestMoreInput();
+ }
+
+ if (span[0] != 0xFF || span[1] != CodestreamMarker)
+ {
+ throw new InvalidOperationException("The file signature is invalid");
+ }
+
+ this.gotCodestreamSignature = true;
+ this.AdvanceCodeStream(2);
+ }
+
+ Span sp = this.GetCodeStreamSpan();
+
+ JxlBitReader bitReader = new(sp);
+
+ if (!this.ReadBundle(sp, bitReader, this.metadata!.Size!))
+ {
+ throw new InvalidDataException("Could not parse the size header");
+ }
+
+ if (!this.ReadBundle(sp, bitReader, this.metadata!.ImageMetadata!))
+ {
+ throw new InvalidDataException("Could not parse the image metadata");
+ }
+
+ long totalBits = bitReader.TotalBitsConsumed;
+
+ this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte);
+
+ this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte;
+ this.gotBasicInfo = true;
+ this.basicInfoSizeHint = 0;
+ this.imageMetadata = this.metadata.ImageMetadata;
+
+ if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize))
+ {
+ throw new InvalidOperationException("The image is too large");
+ }
+
+ return true;
+ }
+
+ ///
+ /// Parses all necessary headers.
+ ///
+ /// Status of the parsing.
+ /// Thrown if data is incorrect.
+ public bool ReadAllHeaders()
+ {
+ if (!this.gotTransformData)
+ {
+ Span span = this.GetCodeStreamSpan();
+
+ JxlBitReader reader = new(span);
+ reader.SkipBits64((ulong)this.codestreamBitsAhead);
+
+ this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded;
+
+ if (!this.ReadBundle(span, reader, this.metadata.CustomTransformData))
+ {
+ throw new InvalidOperationException("Cannot read custom transform data bundle");
+ }
+
+ long totalBits = reader.TotalBitsConsumed;
+ this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte);
+ this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte;
+ this.gotTransformData = true;
+ }
+
+ Span sp = this.GetCodeStreamSpan();
+
+ JxlBitReader bitReader = new(sp);
+ bitReader.SkipBits64((ulong)this.codestreamBitsAhead);
+
+ if (this.metadata!.ImageMetadata!.ColorEncoding!.NeedsIcc)
+ {
+ // TODO: optimize this? ImageSharp ICC doesn't support spans
+ // so we need to allocate an array.
+ IccDataReader reader = new(sp.ToArray());
+ IccProfileHeader header = IccReader.ReadHeader(reader);
+ IccTagDataEntry[] tagData = IccReader.ReadTagData(reader);
+
+ IccProfile icc = new(header, tagData);
+ this.iccProfile = icc;
+ sp = sp[reader.Index..];
+
+ byte[] iccRawData = icc.ToByteArray();
+ this.metadata.ImageMetadata.ColorEncoding.SetIccRaw(iccRawData);
+ }
+
+ this.gotAllHeaders = true;
+ bitReader.JumpToByteBoundary();
+
+ this.AdvanceCodeStream(bitReader.TotalBitsConsumed / JxlMath.BitsPerByte);
+ this.codestreamBitsAhead = 0;
+
+ this.passesState ??= new(this.frameHeader!, this.Options.Configuration);
+ this.passesState.OutputEncodingInfo.SetFromMetadata(this.metadata);
+
+ if (this.desiredIntensityTarget > 0f)
+ {
+ this.passesState.OutputEncodingInfo.DesiredIntensityTarget = this.desiredIntensityTarget;
+ }
+
+ this.imageMetadata = this.metadata.ImageMetadata;
+
+ return true;
+ }
+
+ ///
+ /// Processes all sections in this JPEG XL images and invokes
+ /// the frame decoder.
+ ///
+ /// Thrown if the data is invalid or malformed.
+ public void ProcessSections()
+ {
+ Span span = this.GetCodeStreamSpan();
+
+ var toc = this.frameDecoder!.Toc;
+
+ long pos = 0;
+ List sectionInfo = [];
+ List sectionStatus = [];
+
+ for (long i = this.nextSection; i < toc.Size; i++)
+ {
+ if (this.sectionProcessed[(int)i] != 0)
+ {
+ pos += toc[i].Size;
+ continue;
+ }
+
+ long id = toc[i].Id;
+ long size = toc[i].Size;
+
+ if (IsOutOfBounds((int)pos, (int)size, span.Length))
+ {
+ break;
+ }
+
+ JxlBitReader br = new(span.Slice((int)pos, (int)size));
+ sectionInfo.Add(new(br, id, i));
+ sectionStatus.Add(default);
+ pos += size;
+ }
+
+ this.frameDecoder.ProcessSections(sectionInfo, sectionStatus);
+
+ bool outOfBounds = false;
+
+ foreach (JxlFrameDecoder.SectionInfo info in sectionInfo)
+ {
+ if (!info.BitReader.AllReadsWithinBounds)
+ {
+ outOfBounds = true;
+ break;
+ }
+ }
+
+ if (outOfBounds)
+ {
+ throw new InvalidOperationException("Frame out of bounds");
+ }
+
+ for (int i = 0; i < sectionStatus.Count; i++)
+ {
+ JxlFrameDecoder.SectionStatus ss = sectionStatus[i];
+
+ if (ss == JxlFrameDecoder.Done)
+ {
+ this.sectionProcessed[sectionInfo[i].Index] = 1;
+ }
+ else if (ss != JxlFrameDecoder.Skipped)
+ {
+ throw new InvalidOperationException("Unexpected section status");
+ }
+ }
+
+ long completedPrefixBytes = 0;
+
+ while (this.nextSection < this.sectionProcessed.Count && this.sectionProcessed[(int)this.nextSection] == 1)
+ {
+ completedPrefixBytes += toc[(int)this.nextSection].Size;
+ this.nextSection++;
+ }
+
+ this.remainingFrameSize -= completedPrefixBytes;
+ this.AdvanceCodeStream(completedPrefixBytes);
+ }
+
+ ///
+ /// Processes all codestream contents.
+ ///
+ /// Status of codestream processing.
+ /// Thrown when data is corrupt, malformed, or incorrect.
+ public int ProcessCodestream()
+ {
+ if (!this.gotBasicInfo)
+ {
+ bool status = this.ReadBasicInfo();
+
+ if (!status)
+ {
+ throw new InvalidOperationException("Could not parse basic info");
+ }
+ }
+
+ if ((this.eventsWanted & BasicInfo) != 0)
+ {
+ this.eventsWanted &= ~BasicInfo;
+ return JxlCodestreamType.BasicInfo;
+ }
+
+ if (this.eventsWanted == 0)
+ {
+ this.decoderStage = JxlDecoderStage.CodeStreamFinished;
+ return JxlCodestreamType.Success;
+ }
+
+ if (!this.gotAllHeaders)
+ {
+ bool status = this.ReadAllHeaders();
+
+ if (!status)
+ {
+ throw new InvalidOperationException("Could not parse headers");
+ }
+ }
+
+ if ((this.eventsWanted & ColorEncoding) != 0)
+ {
+ this.eventsWanted &= ~ColorEncoding;
+ return JxlCodestreamType.ColorEncoding;
+ }
+
+ if (this.eventsWanted == 0)
+ {
+ this.decoderStage = JxlDecoderStage.CodeStreamFinished;
+ return JxlCodestreamType.Success;
+ }
+
+ this.postHeaders = true;
+
+ if (!this.gotPreviewImage && this.metadata!.ImageMetadata!.HavePreview)
+ {
+ this.previewFrame = true;
+ }
+
+ while (true)
+ {
+ bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0;
+ if (!parseFrames)
+ {
+ break;
+ }
+
+ if (this.frameStage == JxlFrameStage.Header && this.isLastTotal)
+ {
+ break;
+ }
+
+ if (this.frameStage == JxlFrameStage.Header)
+ {
+ if (this.reconstructionOutputJpeg is JpegReconstructionStage.SetMetadata or JpegReconstructionStage.Output)
+ {
+ throw new InvalidOperationException("Cannot decode frames following a JPEG reconstruction frame");
+ }
+
+ this.imageBundle ??= new(this.imageMetadata!);
+
+ if (!this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!))
+ {
+ throw new InvalidOperationException("Cannot set JXL->JPEG decoder image bundle");
+ }
+
+ this.frameDecoder = new(this.passesState!, this.metadata!, useSlowRenderingPipeline: false);
+ this.frameHeader = new()
+ {
+ Metadata = this.metadata
+ };
+
+ Span span = this.GetCodeStreamSpan();
+ JxlBitReader reader = new(span);
+
+ this.frameDecoder.InitializeFrame(reader, this.imageBundle!, this.previewFrame);
+
+ if (!reader.AllReadsWithinBounds)
+ {
+ return this.TryRequestMoreInput() ? 1 : 0;
+ }
+
+ this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte);
+ this.frameHeader = this.frameDecoder.GetFrameHeader();
+
+ JxlFrameDimensions dim = this.frameHeader.FrameDimensions;
+
+ if (!CheckSizeLimit(dim.XSizeUpsampledPadded, dim.YSizeUpsampledPadded))
+ {
+ throw new InvalidOperationException("Frame is too large");
+ }
+
+ int outputType = this.previewFrame ? PreviewImage : FullImage;
+ bool outputNeeded = (this.eventsWanted & outputType) != 0;
+
+ if (outputNeeded)
+ {
+ this.frameDecoder.InitializeFrameOutput();
+ }
+
+ this.remainingFrameSize = this.frameDecoder.SumSectionSizes();
+
+ this.frameStage = JxlFrameStage.Toc;
+ if (this.previewFrame)
+ {
+ if ((this.eventsWanted & PreviewImage) == 0)
+ {
+ this.frameStage = JxlFrameStage.Header;
+ this.AdvanceCodeStream(this.remainingFrameSize);
+ this.gotPreviewImage = true;
+ this.previewFrame = false;
+ }
+
+ continue;
+ }
+
+ int savedAs = JxlFrameDecoder.SavedAs(this.frameHeader);
+ this.isLastTotal = this.frameHeader.IsLast;
+ this.isLastOfStill = this.isLastTotal || this.frameHeader.AnimationFrame!.Duration > 0;
+ this.isLastOfStill |= !this.coalescing && this.frameHeader.FrameType == JxlFrameType.RegularFrame;
+
+ int internalFrameIndex = this.internalFrames;
+ int externalFrameIndex = this.externalFrames;
+
+ if (this.isLastOfStill)
+ {
+ this.externalFrames++;
+ }
+
+ this.internalFrames++;
+
+ if (this.skipFrames > 0)
+ {
+ this.skippingFrame = true;
+
+ if (this.isLastOfStill)
+ {
+ this.skipFrames--;
+ }
+ }
+ else
+ {
+ this.skippingFrame = false;
+ }
+
+ if (externalFrameIndex >= this.frameExternalToInternal.Count)
+ {
+ this.frameExternalToInternal.Add(internalFrameIndex);
+
+ if (this.frameExternalToInternal.Count != externalFrameIndex + 1)
+ {
+ throw new InvalidOperationException("Internal error");
+ }
+ }
+
+ if (internalFrameIndex >= this.frameReferences.Count)
+ {
+ this.frameReferences.Add(new JxlFrameReference(0xFF, savedAs));
+
+ if (this.frameReferences.Count != internalFrameIndex + 1)
+ {
+ throw new InvalidOperationException("Internal error");
+ }
+ }
+
+ if (this.skippingFrame)
+ {
+ bool referenceable = this.frameHeader.CanBeReferenced
+ || this.frameHeader.FrameType == JxlFrameType.DcFrame;
+
+ if (internalFrameIndex < this.frameRequired.Count && this.frameRequired[internalFrameIndex] == 0)
+ {
+ referenceable = false;
+ }
+
+ if (!referenceable)
+ {
+ this.frameStage = JxlFrameStage.Header;
+ this.AdvanceCodeStream(this.remainingFrameSize);
+ continue;
+ }
+ }
+
+ if ((this.eventsWanted & Frame) != 0 && this.isLastOfStill)
+ {
+ if (!this.skippingFrame)
+ {
+ return Frame;
+ }
+ }
+
+ if (this.frameStage == JxlFrameStage.Toc)
+ {
+ this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors);
+ this.frameDecoder.SetCoalescing(this.coalescing);
+
+ if (!this.previewFrame &&
+ (this.eventsWanted & FrameProgression) != 0)
+ {
+ this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail);
+ }
+ else
+ {
+ this.frameProgressiveDetail = JxlProgressiveDetail.Frames;
+ }
+
+ this.dcFrameProgressionDone = false;
+ this.nextSection = 0;
+ this.sectionProcessed.Clear();
+ ResizeSectionProcessed(this.frameDecoder.Toc.Size);
+
+ if (this.previewFrame || (this.eventsWanted & FullImage) != 0)
+ {
+ this.frameStage = JxlFrameStage.Full;
+ }
+ else if (!this.isLastTotal)
+ {
+ this.frameStage = JxlFrameStage.Header;
+ this.AdvanceCodeStream(this.remainingFrameSize);
+ continue;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (this.frameStage == JxlFrameStage.Full)
+ {
+ if (!this.imageOutBufferSet)
+ {
+ if (this.previewFrame)
+ {
+ return NeedPreviewOutBuffer;
+ }
+
+ if ((!this.jpegDecoder.IsOutputSet || this.imageBundle!.JpegData is null)
+ && this.isLastOfStill
+ && !this.skippingFrame)
+ {
+ return NeedImageOutputBuffer;
+ }
+ }
+
+ if (this.imageOutBufferSet)
+ {
+ Size dimensions = this.CurrentDimensions;
+ int bitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata!.ImageMetadata!, this.imageOutputFormat);
+
+ this.frameDecoder.SetImageOutput(
+ new PixelCallback(
+ this.imageOutputInitCallback,
+ this.imageOutputRunCallback,
+ this.imageOutputDestroyCallback,
+ this.imageOutputInitOpaque),
+ this.imageOutBuffer,
+ this.imageOutputSize,
+ dimensions.Width,
+ dimensions.Height,
+ this.imageOutputFormat,
+ bitsPerSample,
+ this.unpremultiplyAlpha,
+ !this.keepOrientation);
+
+ for (int i = 0; i < this.extraChannelOutputs.Count; i++)
+ {
+ JxlExtraChannelOutput extra = this.extraChannelOutputs[i];
+ int ecBitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata.ImageMetadata!.ExtraChannels[i], extra.Format);
+
+ this.frameDecoder.AddExtraChannelOutput(
+ extra.Buffer,
+ extra.BufferSize,
+ dimensions.Width,
+ extra.Format,
+ ecBitsPerSample);
+ }
+ }
+
+ long nextNumPassesToPause = this.frameDecoder.NextNumPassesToPause;
+
+ this.ProcessSections();
+
+ bool allSectionsDone = this.frameDecoder.DecodedAll;
+ bool gotDcOnly = !allSectionsDone && this.frameDecoder.HasDecodedDc;
+
+ if (this.frameProgressiveDetail >= JxlProgressiveDetail.Dc &&
+ !this.dcFrameProgressionDone &&
+ gotDcOnly)
+ {
+ this.dcFrameProgressionDone = true;
+ this.downsamplingTarget = 8;
+ return Progression;
+ }
+
+ bool newProgressionStepDone = this.frameDecoder.NumCompletePasses >= nextNumPassesToPause;
+
+ if (!allSectionsDone &&
+ this.frameProgressiveDetail >= JxlProgressiveDetail.LastPasses &&
+ newProgressionStepDone)
+ {
+ this.downsamplingTarget = this.frameHeader.Passes.GetDownsamplingTargetForCompletedPasses(this.frameDecoder.NumCompletePasses);
+ return Progression;
+ }
+
+ if (!allSectionsDone)
+ {
+ return this.TryRequestMoreInput() ? 1 : 0;
+ }
+
+ if (!this.previewFrame)
+ {
+ long internalIndex = this.internalFrames - 1;
+ if (this.frameReferences.Count <= internalIndex)
+ {
+ throw new InvalidOperationException("Internal error");
+ }
+
+ this.frameReferences[(int)internalIndex].Reference = this.frameDecoder.References;
+ }
+
+ this.frameDecoder.FinalizeFrame();
+
+ if (this.jpegDecoder.IsOutputSet && this.imageBundle!.JpegData is not null)
+ {
+ this.frameStage = JxlFrameStage.Header;
+ this.reconstructionOutputJpeg = JpegReconstructionStage.SetMetadata;
+
+ return FullImage;
+ }
- ///
- /// Container format.
- ///
- Container
+ if (this.previewFrame || this.isLastOfStill)
+ {
+ this.imageOutBufferSet = false;
+ this.extraChannelOutputs.Clear();
+ }
+ }
+
+ this.frameStage = JxlFrameStage.Header;
+ this.imageBundle.Reset();
+
+ if (this.previewFrame)
+ {
+ this.gotPreviewImage = true;
+ this.previewFrame = false;
+ this.eventsWanted &= ~PreviewImage;
+ return PreviewImage;
+ }
+ else if (this.isLastOfStill && (this.eventsWanted & FullImage) != 0 && !this.skippingFrame)
+ {
+ return FullImage;
+ }
+ }
+ }
+
+ this.decoderStage = JxlDecoderStage.CodeStreamFinished;
+ return 1;
}
///
- /// Represents a data type.
+ /// Sets the input JPEG XL data to .
///
- private enum JxlDataType : byte
+ /// The input data to parse JPEG XL.
+ /// Thrown if the input data cannot be changed at this moment.
+ public void SetInput(IMemoryOwner data)
{
- ///
- ///
- ///
- UInt8,
-
- ///
- ///
- ///
- UInt16,
+ if (this.nextInput is not null)
+ {
+ throw new InvalidOperationException("Input is already present. Use DisposeInput first");
+ }
- ///
- ///
- ///
- Float,
+ if (this.inputClosed)
+ {
+ throw new InvalidOperationException("Input is closed");
+ }
- ///
- ///
- ///
- Float16
+ this.nextInput = data;
+ this.availableInput = data.Memory.Length;
}
- public JxlDecoderCore(DecoderOptions options)
- : base(options)
+ ///
+ /// Disposes the input data.
+ ///
+ ///
+ /// Number of available bytes left in the input data before disposal.
+ ///
+ public long DisposeInput()
{
+ long previousAvailableBytes = this.availableInput;
+
+ this.nextInput?.Dispose();
+ this.nextInput = null;
+ this.availableInput = 0;
+
+ return previousAvailableBytes;
}
///
- /// Ensures that the coordinates are not out of bounds.
+ /// Closes the input stream so it can't be read anymore.
///
- /// First coordinate
- /// Second coordinate
- /// Image width
- /// Boolean indicating whether the coordinates are out of bounds
- private static bool IsOutOfBounds(int a, int b, int size)
+ public void CloseInput() => this.inputClosed = true;
+
+ ///
+ /// Sets the output buffer for JPEG reconstruction.
+ ///
+ /// Buffer for JPEG reconstruction.
+ /// Thrown when the buffer can't be set.
+ public void SetJpegBuffer(Memory data)
{
- long position = a + b;
+ if (this.internalFrames > 1)
+ {
+ throw new InvalidOperationException("JPEG reconstruction only works for first frames");
+ }
- return position > size || position < a;
- }
+ if (this.jpegDecoder.IsOutputSet)
+ {
+ throw new InvalidOperationException("Already set JPEG buffer");
+ }
- private static int InitialBasicInfoSizeHint()
- {
- const int containerHeaderSize = 48;
- const int maxCodestreamBasicInfoSize = 50;
- return containerHeaderSize + maxCodestreamBasicInfoSize;
+ this.jpegDecoder.SetOutputBuffer(data);
}
- private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position)
+ ///
+ /// Parses the start of a box.
+ ///
+ /// Input bytes to parse from.
+ /// Size of remaining input bytes.
+ /// Offset of input bytes.
+ /// File offset.
+ /// Type of the parsed box.
+ /// Output box size.
+ /// Output header size.
+ ///
+ /// True if the parsing went fine. False if the parsing requests
+ /// more input bytes.
+ ///
+ ///
+ /// Thrown when data is invalid.
+ ///
+ private static bool ParseBoxHeader(Span input, long size, long pos, long filePos, JxlBoxType type, out long boxSize, out long headerSize)
{
- if (position >= length)
+ boxSize = 0;
+ headerSize = 0;
+
+ if (IsOutOfBounds((int)pos, 8, (int)size))
{
- return JxlSignature.NotEnoughBytes;
+ headerSize = 8;
+ return false;
}
- buffer = buffer[position..];
- length -= position;
+ long boxStart = pos;
+ boxSize = BinaryPrimitives.ReadInt32BigEndian(input[(int)pos..]);
+ pos += 4;
+ type = (JxlBoxType)BitConverter.ToInt32(input.Slice((int)pos, 4));
+ pos += 4;
- // 0xFF 0x0A represents a codestream
- if (length >= 1 && buffer[0] == 0xFF)
+ if (boxSize == 1)
{
- if (length < 2)
+ headerSize = 16;
+
+ if (IsOutOfBounds((int)pos, 8, (int)size))
{
- // We need at least two bytes for a valid codestream signature
- return JxlSignature.NotEnoughBytes;
+ return false;
}
- else if (buffer[1] == CodestreamMarker)
+
+ long boxSize64 = BinaryPrimitives.ReadInt64BigEndian(input[(int)pos..]);
+ pos += 8;
+ boxSize = boxSize64;
+ }
+
+ headerSize = pos - boxStart;
+
+ if (boxSize > 0 && boxSize < headerSize)
+ {
+ throw new InvalidOperationException("Invalid box size");
+ }
+
+ if (filePos + boxSize < filePos)
+ {
+ throw new InvalidOperationException("Box size overflow");
+ }
+
+ return true;
+ }
+
+ ///
+ /// Processes all boxes and their contents if this is a container format.
+ ///
+ /// Status of processing.
+ /// Thrown when data is invalid.
+ public int ProcessBoxes()
+ {
+ // We have a box handling loop here.
+ while (true)
+ {
+ if (this.boxStage != JxlBoxStage.Header)
{
- position += 2;
- return JxlSignature.CodeStream;
+ this.AdvanceInput(this.headerSize);
+ this.headerSize = 0;
+
+ if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox)
+ {
+ this.boxEvent = false;
+ }
+
+ if ((this.eventsWanted & Box) != 0 && this.boxOutBufferSetCurrentBox)
+ {
+ Memory nextOut = this.boxOutputBuffer!.Memory[(int)this.boxOutBufferPos..];
+ long availOut = this.boxOutBufferSize - this.boxOutBufferPos;
+
+ Span bufferSpan = this.boxOutputBuffer.Memory.Span;
+ Span startSlice = bufferSpan[(int)this.boxOutBufferPos..];
+
+ int status = this.boxContentDecoder!.Process(
+ this.nextInput,
+ this.availableInput,
+ this.filePosition - this.boxContentsBegin,
+ nextOut,
+ ref availOut);
+
+ long produced = startSlice.Length - availOut;
+ this.boxOutBufferPos += produced;
+
+ if (status == Complete && (this.eventsWanted & Complete) == 0)
+ {
+ status = Success;
+ }
+
+ if (status is not (Success or NeedMoreInput))
+ {
+ return status;
+ }
+ }
+
+ if (this.storeExif == 1 || this.storeXmp == 1)
+ {
+ IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) ?? throw new InvalidOperationException("Metadata is missing, but should be present");
+
+ // Boxes should not contain more than 64MiB data.
+ const long blockSizeLimit = 64L << 20;
+
+ // Use array version of metadata so we
+ // can resize the array.
+ byte[] md = metadata.Memory.ToArray();
+
+ while (true)
+ {
+ if (md.Length == 0)
+ {
+ Array.Resize(ref md, 64);
+ }
+
+ Span originalNextOutput = md.AsSpan()[(int)this.reconstructionOutputBufferPos..];
+ Span nextOutput = originalNextOutput;
+ long availableOutput = md.Length - this.reconstructionOutputBufferPos;
+
+ int boxResult = this.metadataDecoder.Decode(
+ this.nextInput,
+ this.availableInput,
+ this.filePosition - this.boxContentsBegin,
+ ref nextOutput,
+ ref availableOutput);
+
+ long produced = originalNextOutput.Length - nextOutput.Length;
+ this.reconstructionOutputBufferPos += produced;
+
+ if (boxResult == NeedMoreOutput)
+ {
+ if (md.Length >= blockSizeLimit)
+ {
+ throw new InvalidOperationException("Box with EXIF or XMP metadata is too large");
+ }
+
+ Array.Resize(ref md, md.Length * 2);
+ }
+ else if (boxResult == NeedMoreInput)
+ {
+ break;
+ }
+ else if (boxResult == Complete)
+ {
+ long neededSize = this.storeExif == 1 ? this.reconstructionExifSize : this.reconstructionXmpSize;
+
+ if (this.boxContentsUnbounded && this.reconstructionOutputBufferPos < neededSize)
+ {
+ break;
+ }
+ else
+ {
+ Array.Resize(ref md, (int)this.reconstructionOutputBufferPos);
+
+ if (this.storeExif == 1)
+ {
+ this.storeExif = 2;
+ }
+
+ if (this.storeXmp == 1)
+ {
+ this.storeXmp = 2;
+ }
+
+ break;
+ }
+ }
+ else
+ {
+ // Error
+ return boxResult;
+ }
+ }
+ }
+ }
+
+ if (this.reconstructionOutputJpeg == JpegReconstructionStage.SetMetadata && this.JbrdNeedsMoreBoxes())
+ {
+ JxlJpegData jpegData = this.imageBundle!.JpegData.GetData();
+
+ if (this.reconstructionExifSize > 0)
+ {
+ int status = JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData);
+ if (status != Success)
+ {
+ return status;
+ }
+ }
+
+ if (this.reconstructionXmpSize > 0)
+ {
+ int status = JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData);
+ if (status != Success)
+ {
+ return status;
+ }
+ }
+
+ this.reconstructionOutputJpeg = JpegReconstructionStage.Output;
+ }
+
+ if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes())
+ {
+ int status = this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData);
+ if (status != Success)
+ {
+ return status;
+ }
+
+ this.reconstructionOutputJpeg = JpegReconstructionStage.None;
+ this.imageBundle.Reset();
+
+ if ((this.eventsWanted & FullImage) != 0)
+ {
+ return FullImage;
+ }
+ }
+
+ if (this.boxStage == JxlBoxStage.Header)
+ {
+ if (!this.haveContainer)
+ {
+ if (this.decoderStage == JxlDecoderStage.CodeStreamFinished)
+ {
+ return Success;
+ }
+
+ this.boxStage = JxlBoxStage.CodeStream;
+ this.boxContentsUnbounded = true;
+
+ continue;
+ }
+
+ if (this.availableInput == 0)
+ {
+ if (this.decoderStage != JxlDecoderStage.CodeStreamFinished)
+ {
+ return NeedMoreInput;
+ }
+
+ if (this.JbrdNeedsMoreBoxes())
+ {
+ return NeedMoreInput;
+ }
+
+ if (this.inputClosed)
+ {
+ return Success;
+ }
+
+ if ((this.eventsWanted & Box) != 0)
+ {
+ return Success;
+ }
+
+ return NeedMoreInput;
+ }
+
+ bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0)
+ && this.decoderStage == JxlDecoderStage.CodeStreamFinished
+ && !this.JbrdNeedsMoreBoxes()
+ && this.lastCodestreamSeen;
+
+ if (boxedCodestreamDone &&
+ this.availableInput >= 2 &&
+ this.nextInput!.Memory.Span[0] == 0xFF &&
+ this.nextInput.Memory.Span[1] == CodestreamMarker)
+ {
+ return Success;
+ }
+
+ int status = ParseBoxHeader(this.nextInput, this.availableInput, 0, this.filePosition, this.boxType, out long boxSize, out long headerSize);
+
+ if (this.boxType == JxlBoxTypes.Brob)
+ {
+ if (this.availableInput < headerSize + 4)
+ {
+ return NeedMoreInput;
+ }
+
+ this.boxDecodedType = BitConverter.ToInt32(this.nextInput!.Memory.Span[(int)headerSize..]);
+ }
+ else
+ {
+ this.boxDecodedType = this.boxType;
+ }
+
+ this.boxCount++;
+
+ if (boxedCodestreamDone && this.boxType == JxlBoxTypes.Jxl)
+ {
+ return Success;
+ }
+
+ if (this.boxCount == 2 && this.boxType != JxlBoxType.FileType)
+ {
+ throw new InvalidOperationException("The second box must be a ftyp (File Type) box");
+ }
+
+ if (this.boxType == JxlBoxTypes.FileType && this.boxCount != 2)
+ {
+ throw new InvalidOperationException("The ftyp (File Type) box must be a second box");
+ }
+
+ this.boxContentsUnbounded = boxSize == 0;
+ this.boxContentsBegin = this.filePosition + headerSize;
+ this.boxContentsEnd = this.boxContentsUnbounded ? 0 : (this.filePosition + boxSize);
+ this.boxContentsSize = this.boxContentsUnbounded ? 0 : (boxSize - headerSize);
+ this.boxSize = boxSize;
+ this.headerSize = headerSize;
+
+ if ((this.originalEventsWanted & JpegReconstruction) != 0)
+ {
+ if (this.storeExif == 0 && this.boxDecodedType == JxlBoxTypes.Exif)
+ {
+ this.storeExif = 1;
+ this.reconstructionOutputBufferPos = 0;
+ }
+
+ if (this.storeXmp == 0 && this.boxDecodedType == JxlBoxTypes.Xml)
+ {
+ this.storeXmp = 1;
+ this.reconstructionOutputBufferPos = 0;
+ }
+ }
+
+ if ((this.eventsWanted & Box) != 0)
+ {
+ bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob;
+ this.boxContentDecoder.StartBox(decompress, this.boxContentsUnbounded, this.boxContentsSize);
+ }
+
+ if (this.storeExif == 1 || this.storeXmp == 1)
+ {
+ bool brob = this.boxType == JxlBoxTypes.Brob;
+ this.metadataDecoder.StartBox(brob, this.boxContentsUnbounded, this.boxContentsSize);
+ }
+
+ if (this.boxType == JxlBoxTypes.FileType)
+ {
+ this.boxStage = JxlBoxStage.Ftyp;
+ }
+ else if (this.boxType == JxlBoxTypes.JxlCodeStream)
+ {
+ if (this.lastCodestreamSeen)
+ {
+ throw new InvalidOperationException("Only one jxlc (JPEG XL codestream) box can be present");
+ }
+
+ this.lastCodestreamSeen = true;
+ this.boxStage = JxlBoxStage.CodeStream;
+ }
+ else if (this.boxType == JxlBoxTypes.JxlPartialCodeStream)
+ {
+ this.boxStage = JxlBoxStage.PartialCodeStream;
+ }
+ else if ((this.originalEventsWanted & JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData)
+ {
+ if ((this.eventsWanted & JpegReconstruction) == 0)
+ {
+ throw new InvalidOperationException("Multiple JPEG reconstruction boxes detected");
+ }
+
+ this.boxStage = JxlBoxStage.JpegReconstruction;
+ }
+ else
+ {
+ this.boxStage = JxlBoxStage.Skip;
+ }
+
+ if ((this.eventsWanted & Box) != 0)
+ {
+ this.boxEvent = true;
+ this.boxOutBufferSetCurrentBox = false;
+ return Box;
+ }
+ }
+ else if (this.boxStage == JxlBoxStage.Ftyp)
+ {
+ if (this.boxContentsSize < 12)
+ {
+ throw new InvalidOperationException("The file type box is too small");
+ }
+
+ if (this.availableInput < 8)
+ {
+ return NeedMoreInput;
+ }
+
+ Span nextSpan = this.nextInput!.Memory.Span;
+ if (!(nextSpan[0] == 'j' && nextSpan[1] == 'x' && nextSpan[2] == 'l' && nextSpan[3] == ' '))
+ {
+ throw new InvalidOperationException("File type box major brand must be \"jxl \"");
+ }
+
+ uint version = BinaryPrimitives.ReadUInt32BigEndian(nextSpan[4..]);
+ if (version > 1)
+ {
+ throw new InvalidOperationException("Unknown JXL file format version " + version + ", known versions are 0 and 1");
+ }
+
+ this.jxlFileFormatVersion = (int)version;
+ this.AdvanceInput(8);
+ this.boxStage = JxlBoxStage.Skip;
+ }
+ else if (this.boxStage == JxlBoxStage.PartialCodeStream)
+ {
+ if (this.lastCodestreamSeen)
+ {
+ throw new InvalidOperationException("Cannot have jxlp box after last jxlp box");
+ }
+
+ if (this.availableInput < 4)
+ {
+ return NeedMoreInput;
+ }
+
+ if (!this.boxContentsUnbounded && this.boxContentsSize < 4)
+ {
+ throw new InvalidOperationException("jxlp box is too small to contain an index");
+ }
+
+ uint jxlpIndex = BinaryPrimitives.ReadUInt32BigEndian(this.nextInput!.Memory.Span);
+ uint counter = jxlpIndex & 0x7FFFFFFFu;
+ bool isLast = (jxlpIndex & 0x80000000u) != 0;
+
+ if (counter < this.nextJxlpIndex)
+ {
+ throw new InvalidOperationException("jxlp box index " + counter + " is a duplicate (already processed)");
+ }
+
+ this.AdvanceInput(4);
+
+ if (counter == this.nextJxlpIndex)
+ {
+ this.nextJxlpIndex++;
+
+ if (isLast)
+ {
+ this.lastCodestreamSeen = true;
+ }
+
+ this.boxStage = JxlBoxStage.CodeStream;
+ }
+ else if (this.jxlFileFormatVersion >= 1)
+ {
+ if (this.jxlpOooBuffer.Count >= NumBuffersLimit)
+ {
+ return Error;
+ }
+
+ // When creating a new OOO (Out-of-order) entry,
+ // the data is initially empty.
+ byte[] buffer = [];
+ JxlOooEntry entry = new(buffer, isLast);
+ this.jxlpOooBuffer.Add((int)counter, entry);
+
+ this.bufferingJxlpIndex = (int)counter;
+ this.bufferingJxlpIsLast = isLast;
+ this.boxStage = JxlBoxStage.BufferingJxlp;
+ }
+ else
+ {
+ throw new InvalidOperationException("JXLP box with index " + counter + " is out of order (index " + this.nextJxlpIndex + " was expected). Out-of-order jxlp boxes require file format version 1 in the file type (ftyp) box.");
+ }
+ }
+ else if (this.boxStage == JxlBoxStage.CodeStream)
+ {
+ int status = this.ProcessCodestream();
+
+ if (status == FullImage)
+ {
+ if (this.reconstructionOutputJpeg != JpegReconstructionStage.None)
+ {
+ continue;
+ }
+ }
+
+ if (status == NeedMoreInput)
+ {
+ if (this.filePosition == this.boxContentsEnd && !this.boxContentsUnbounded)
+ {
+ bool hasMoreData = this.TryInjectNextBufferedJxlpBox();
+
+ if (hasMoreData)
+ {
+ continue;
+ }
+
+ this.boxStage = JxlBoxStage.Header;
+ continue;
+ }
+ }
+
+ if (status == Success)
+ {
+ if (this.JbrdNeedsMoreBoxes())
+ {
+ this.boxStage = JxlBoxStage.Skip;
+ continue;
+ }
+
+ if (this.boxContentsUnbounded)
+ {
+ break;
+ }
+
+ if ((this.eventsWanted & Box) != 0)
+ {
+ this.boxStage = JxlBoxStage.Skip;
+ continue;
+ }
+ }
+
+ return status;
+ }
+ else if (this.boxStage == JxlBoxStage.BufferingJxlp)
+ {
+ long remaining = this.boxContentsUnbounded
+ ? this.availableInput
+ : Math.Min(this.availableInput, this.boxContentsEnd - this.filePosition);
+
+ if (!this.CanAddBuffer(remaining) || !this.jxlpOooBuffer.TryGetValue(this.bufferingJxlpIndex, out JxlOooEntry? entry))
+ {
+ return Error;
+ }
+
+ entry!.CodestreamBytes.Write(this.nextInput!.Memory.Span[..(int)remaining]);
+ this.jxlpOooBufferTotal += remaining;
+ this.AdvanceInput(remaining);
+
+ bool boxDone = !this.boxContentsUnbounded && this.filePosition >= this.boxContentsEnd;
+
+ if (!boxDone)
+ {
+ return NeedMoreInput;
+ }
+
+ this.boxStage = JxlBoxStage.Header;
+ }
+ else if (this.boxStage == JxlBoxStage.JpegReconstruction)
+ {
+ if (!this.jpegDecoder.IsParsingBox)
+ {
+ this.jpegDecoder.StartBox(this.boxContentsUnbounded, this.boxContentsSize);
+ }
+
+ Span nextInput = this.nextInput!.Memory.Span;
+ long availableInput = this.availableInput;
+
+ int reconstructionResult = this.jpegDecoder.Process(ref nextInput, ref availableInput);
+
+ long consumed = this.nextInput.Memory.Length - nextInput.Length;
+ this.AdvanceInput(consumed);
+
+ if (reconstructionResult == JpegReconstruction)
+ {
+ JxlJpegData jpegData = this.jpegDecoder!.GetJpegData();
+ long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData);
+ long numXmp = JxlToJpegDecoder.NumXmpMarkers(jpegData);
+
+ if (numExif > 0)
+ {
+ if (numExif > 1)
+ {
+ throw new InvalidOperationException("Only one EXIF marker for JPEG reconstruction can be present");
+ }
+
+ if (JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize) != Success)
+ {
+ throw new InvalidOperationException("Invalid jbrd EXIF size");
+ }
+ }
+
+ if (numXmp > 0)
+ {
+ if (numXmp > 1)
+ {
+ throw new InvalidOperationException("Only one XMP marker for JPEG reconstruction can be present");
+ }
+
+ if (JxlToJpegDecoder.XmlBoxContentSize(jpegData, ref this.reconstructionXmpSize) != Success)
+ {
+ throw new InvalidOperationException("Invalid jbrd XMP size");
+ }
+ }
+
+ this.boxStage = JxlBoxStage.Header;
+
+ if ((this.eventsWanted & JpegReconstruction) != 0)
+ {
+ this.eventsWanted &= ~JpegReconstruction;
+ return JpegReconstruction;
+ }
+ }
+ else
+ {
+ return reconstructionResult;
+ }
+ }
+ else if (this.boxStage == JxlBoxStage.Skip)
+ {
+ if (this.boxContentsUnbounded)
+ {
+ if (this.inputClosed)
+ {
+ return Success;
+ }
+
+ if (!this.boxOutBufferSet)
+ {
+ return Success;
+ }
+
+ this.AdvanceInput(this.availableInput);
+ return NeedMoreInput;
+ }
+
+ long remaining = this.boxContentsEnd - this.filePosition;
+ if (this.availableInput < remaining)
+ {
+ this.basicInfoSizeHint = InitialBasicInfoSizeHint() + this.boxContentsEnd - this.filePosition;
+ this.AdvanceInput(this.availableInput);
+ return NeedMoreInput;
+ }
+ else
+ {
+ this.AdvanceInput(remaining);
+ this.boxStage = JxlBoxStage.Header;
+ }
}
else
{
- return JxlSignature.Invalid;
+ throw new InvalidOperationException("Unreachable");
}
}
- // Container?
- if (length >= 1 && buffer[0] == 0)
+ return Success;
+ }
+
+ ///
+ /// Releases memory used by the JPEG output buffer.
+ ///
+ public void DisposeJpegBuffer() => this.jpegDecoder.DisposeOutputBuffer();
+
+ ///
+ /// Main core decoding routine.
+ ///
+ /// Thrown when data or input parameters are invalid.
+ public void DecodeInput()
+ {
+ if (this.decoderStage == JxlDecoderStage.Initialized)
{
- if (length < SignatureBox.Length)
+ this.decoderStage = JxlDecoderStage.Started;
+ }
+
+ if (this.decoderStage == JxlDecoderStage.Error)
+ {
+ // Should NEVER occur! If it does make sure to always reset the decoder
+ // in the Decode method.
+ throw new InvalidOperationException("The core decoder cannot be used because it contains an error. A reset must be made.");
+ }
+
+ if (!this.gotSignature)
+ {
+ JxlSignatureCheck status = CheckSignature(this.nextInput, this.availableInput);
+ if (status == JxlSignatureCheck.InvalidSignature)
{
- return JxlSignature.NotEnoughBytes;
+ throw new InvalidOperationException("The signature is invalid.");
}
- else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox))
+
+ if (status == JxlSignatureCheck.NotEnoughBytes)
{
- position += SignatureBox.Length;
- return JxlSignature.Container;
+ if (this.inputClosed)
+ {
+ throw new InvalidOperationException("The input is closed");
+ }
+
+ ThrowNotEnoughData();
+ }
+
+ this.gotSignature = true;
+
+ if (status == JxlSignatureCheck.Container)
+ {
+ this.haveContainer = true;
}
else
{
- return JxlSignature.Invalid;
+ this.lastCodestreamSeen = true;
}
}
- // Signature is invalid
- return JxlSignature.Invalid;
- }
+ int status = this.ProcessBoxes();
- private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length)
- {
- int position = 0;
- return DetectSignature(buffer, length, ref position);
- }
+ if (status == NeedMoreInput && this.inputClosed)
+ {
+ ThrowNotEnoughData();
+ }
- private static int BitsPerChannel(JxlDataType dataType)
- => dataType switch
+ if (status == Success)
{
- JxlDataType.UInt8 => 8,
- JxlDataType.UInt16 or JxlDataType.Float16 => 16,
- JxlDataType.Float => 32,
- _ => 0
- };
+ if (this.CanUseMoreCodestreamInput())
+ {
+ throw new InvalidOperationException("The code stream did not finish");
+ }
+
+ if (this.JbrdNeedsMoreBoxes())
+ {
+ throw new InvalidOperationException("Missing metadata boxes for JPEG reconstruction");
+ }
+ }
+ }
protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException();
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs
new file mode 100644
index 000000000..89f5af5e3
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Specifies the type of box content.
+///
+internal enum JxlBoxCodingMode : byte
+{
+ ///
+ /// Compress using Brotli codec.
+ ///
+ Brotli,
+
+ ///
+ /// No compression (raw contents).
+ ///
+ Uncompressed
+}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs
index c5464c8d7..faa94dea4 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs
@@ -3,6 +3,8 @@
namespace SixLabors.ImageSharp.Metadata.Profiles.Icc;
+#pragma warning disable IDE0032 // Use auto property
+
///
/// Provides methods to read ICC data types
///
@@ -25,6 +27,11 @@ internal sealed partial class IccDataReader
public IccDataReader(byte[] data)
=> this.data = data ?? throw new ArgumentNullException(nameof(data));
+ ///
+ /// Gets the reading position.
+ ///
+ public int Index => this.currentIndex;
+
///
/// Gets the length in bytes of the raw data
///
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
index 084ec388d..d46a7332b 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
@@ -53,7 +53,7 @@ internal sealed class IccReader
return ReadTagData(reader);
}
- private static IccProfileHeader ReadHeader(IccDataReader reader)
+ internal static IccProfileHeader ReadHeader(IccDataReader reader)
{
reader.SetIndex(0);
@@ -79,7 +79,7 @@ internal sealed class IccReader
};
}
- private static IccTagDataEntry[] ReadTagData(IccDataReader reader)
+ internal static IccTagDataEntry[] ReadTagData(IccDataReader reader)
{
IccTagTableEntry[] tagTable = ReadTagTable(reader);
List entries = new(tagTable.Length);