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