From fed0af3254d4c547f9002897f2ed732f0cd09e14 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Thu, 16 Jul 2026 22:15:46 +0400
Subject: [PATCH] Add bit-stream reader
---
src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 165 ++++++++++++++++++
src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs | 14 ++
2 files changed, 179 insertions(+)
create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
create mode 100644 src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
new file mode 100644
index 0000000000..55e3ed3efd
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// Represents a bitstream reader.
+///
+internal sealed class JxlBitReader(ReadOnlyMemory bytes)
+{
+ private ulong buffer;
+ private uint bufferRemainingBits;
+ private int pointer;
+ private bool endOfStream;
+
+ ///
+ /// Fetches a new buffer.
+ ///
+ private void RefillCore()
+ {
+ ReadOnlySpan samplesSpan = bytes.Span;
+
+ int remaining = samplesSpan.Length - this.pointer;
+ if (remaining <= 0)
+ {
+ // we don't have any more data... mark an end of stream
+ this.buffer = 0;
+ this.bufferRemainingBits = 0;
+ this.endOfStream = true;
+ return;
+ }
+
+ if (remaining >= 8)
+ {
+ this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]);
+ this.bufferRemainingBits = 64u;
+ this.pointer += 8;
+ }
+ else
+ {
+ ulong value = 0;
+ for (int i = 0; i < remaining; i++)
+ {
+ value |= (ulong)samplesSpan[this.pointer + i] << (8 * i);
+ }
+
+ this.buffer = value;
+ this.bufferRemainingBits = (uint)(remaining * 8);
+ this.pointer += remaining;
+ }
+ }
+
+ private void MaybeRefill()
+ {
+ if (this.bufferRemainingBits <= 0)
+ {
+ this.RefillCore();
+ }
+ }
+
+ private ulong ReadBits64Core(uint n, bool peek = false)
+ {
+ Debug.Assert(n <= 64, "Too many bits to pack into ulong");
+ this.MaybeRefill();
+
+ if (this.endOfStream)
+ {
+ JxlThrowHelper.ThrowEndOfStream();
+ }
+
+ if (n <= this.bufferRemainingBits)
+ {
+ ulong result = this.buffer & ((1UL << (int)n) - 1);
+
+ if (!peek)
+ {
+ this.buffer >>= (int)n;
+ this.bufferRemainingBits -= n;
+ }
+
+ return result;
+ }
+ else
+ {
+ uint bitsFromCurrent = this.bufferRemainingBits;
+ ulong part = this.buffer & ((1UL << (int)bitsFromCurrent) - 1);
+
+ this.buffer >>= (int)bitsFromCurrent;
+ this.bufferRemainingBits = 0;
+
+ this.RefillCore();
+
+ uint bitsFromNext = n - bitsFromCurrent;
+ ulong nextPart = this.buffer & ((1UL << (int)bitsFromNext) - 1);
+
+ if (!peek)
+ {
+ this.buffer >>= (int)bitsFromNext;
+ this.bufferRemainingBits -= bitsFromNext;
+ }
+
+ return part | (nextPart << (int)bitsFromCurrent);
+ }
+ }
+
+ private uint ReadBits32Core(uint n, bool peek = false)
+ {
+ Debug.Assert(n <= 32, "Too many bits to pack into uint");
+ this.MaybeRefill();
+
+ if (this.endOfStream)
+ {
+ JxlThrowHelper.ThrowEndOfStream();
+ }
+
+ if (n <= this.bufferRemainingBits)
+ {
+ uint result = (uint)(this.buffer & ((1UL << (int)n) - 1));
+
+ if (!peek)
+ {
+ this.buffer >>= (int)n;
+ this.bufferRemainingBits -= n;
+ }
+
+ return result;
+ }
+ else
+ {
+ uint bitsFromCurrent = this.bufferRemainingBits;
+ uint part = (uint)(this.buffer & ((1UL << (int)bitsFromCurrent) - 1));
+
+ this.buffer >>= (int)bitsFromCurrent;
+ this.bufferRemainingBits = 0;
+
+ this.RefillCore();
+
+ uint bitsFromNext = n - bitsFromCurrent;
+ uint nextPart = (uint)(this.buffer & ((1UL << (int)bitsFromNext) - 1));
+
+ if (!peek)
+ {
+ this.buffer >>= (int)bitsFromNext;
+ this.bufferRemainingBits -= bitsFromNext;
+ }
+
+ return part | (nextPart << (int)bitsFromCurrent);
+ }
+ }
+
+ public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false);
+
+ public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true);
+
+ public void SkipBits32(uint bits) => _ = this.ReadBits32(bits);
+
+ public ulong ReadBits64(uint bits) => this.ReadBits64Core(bits, peek: false);
+
+ public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true);
+
+ public void SkipBits64(uint bits) => _ = this.ReadBits64(bits);
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
new file mode 100644
index 0000000000..239d36cd01
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal static class JxlThrowHelper
+{
+ private static readonly EndOfStreamException EndOfStream = new();
+
+ [DoesNotReturn]
+ public static void ThrowEndOfStream() => throw EndOfStream;
+}