From 9f8eb4a434c7386bdb6bebdc2d1bcc49e84ac30e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:34:40 +0400 Subject: [PATCH] Add box content decoder with Brotli compression & reduce errors --- .../Decoder/JxlBoxContentDecoder.cs | 108 ++++++++++++++++++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 10 +- 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs new file mode 100644 index 000000000..7621102ef --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.IO.Compression; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Allows decoding and decompressing box data. +/// +internal sealed class JxlBoxContentDecoder +{ + /// + /// Specifies how many bytes to read to fetch box data. This is ignored + /// if the box extends till EOF. + /// + private ulong boxSize; + + /// + /// When true the box size is ignored and is unbounded - that is, keeps going + /// till the end of the file or stream. + /// + private bool boxExtendsTillEnd; + + /// + /// This contains flags that determine whether the box is Brotli-compressed or + /// not. + /// + private JxlBoxCodingMode codingMode; + + /// + /// Prepares parsing the box. + /// + /// Specifies box compression. + /// Specifies whether or not the box size keeps going till the end of stream. + /// Specifies the fixed size of the box when it is not unbounded. + public void Initialize(JxlBoxCodingMode codingMode, bool isUnbounded, ulong size) + { + this.boxSize = size; + this.codingMode = codingMode; + this.boxExtendsTillEnd = isUnbounded; + } + + /// + /// Prepares parsing the box. + /// + /// True if the box is compressed with Brotli. If uncompressed - false. + /// Specifies whether or not the box size keeps going till the end of stream. + /// Specifies the fixed size of the box when it is not unbounded. + public void Initialize(bool isBrotliCompressed, bool isUnbounded, ulong size) + => this.Initialize( + isBrotliCompressed ? JxlBoxCodingMode.Brotli : JxlBoxCodingMode.Uncompressed, + isUnbounded, + size); + + public void Process(Stream stream, JxlMemoryWriter writer) + { + byte[] cache = ArrayPool.Shared.Rent(16384); + + try + { + if (this.codingMode == JxlBoxCodingMode.Brotli) + { + using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + + int bytesRead; + while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + if (this.boxExtendsTillEnd) + { + int bytesRead; + while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + ulong bytesLeft = this.boxSize; + while (bytesLeft > 0) + { + int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); + int bytesRead = stream.Read(cache, 0, toRead); + + if (bytesRead == 0) + { + throw new EndOfStreamException("Unexpected EOF while reading box content"); + } + + writer.Write(cache.AsSpan(0, bytesRead)); + bytesLeft -= (ulong)bytesRead; + } + } + } + } + finally + { + ArrayPool.Shared.Return(cache); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 016269e4d..7ca3ace0e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1508,7 +1508,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable Span fileSignature = stackalloc byte[2]; stream.ReadExactly(fileSignature); - if (fileSignature[0] != 0xFF || fileSignature[1] != CodestreamMarker) + if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) { throw new InvalidOperationException("The file signature is invalid"); } @@ -1553,7 +1553,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable { Span span = this.GetCodeStreamSpan(); - JxlBitReader reader = new(span); + JxlBitReader reader = new(this.stream); reader.SkipBits64((ulong)this.codestreamBitsAhead); this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded; @@ -2286,7 +2286,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable { if (this.decoderStage != JxlDecoderStage.CodeStreamFinished || this.JbrdNeedsMoreBoxes()) { - return NeedMoreInput; + ThrowNotEnoughData(); } if (this.inputClosed || (this.eventsWanted & Box) != 0) @@ -2294,7 +2294,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable return Success; } - return NeedMoreInput; + ThrowNotEnoughData(); } bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) @@ -2556,7 +2556,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable if (!boxDone) { - return NeedMoreInput; + ThrowNotEnoughData(); } this.boxStage = JxlBoxStage.Header;