Skip to content

Support for JPEG XL (JXL) images - #3153

Draft
winscripter wants to merge 122 commits into
SixLabors:mainfrom
winscripter:jxl-support
Draft

Support for JPEG XL (JXL) images#3153
winscripter wants to merge 122 commits into
SixLabors:mainfrom
winscripter:jxl-support

Conversation

@winscripter

@winscripter winscripter commented Jul 15, 2026

Copy link
Copy Markdown

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This is a work-in-progress PR whose goal is to introduce decoding and encoding of JPEG XL (*.jxl) images.

Reference software
I use libjxl as reference. See https://github.com/libjxl/libjxl.

Performance
I will begin by applying light optimizations as I implement parts of the JPEG XL codec. Once the codec seems complete enough to handle decoding and encoding of JPEG XL images, I will apply heavier optimizations. Examples include but are not limited to stack allocation, array pooling, and SIMD.

Implementations
The JPEG XL codec lives under src/ImageSharp/Formats/Jxl.

Testing
I will start adding tests whenever the codec is complete enough to handle decoding of JPEG XL images.

Additionally, JPEG XL reference software, libjxl, contains its own tests too, which I might also implement without modification.

@CLAassistant

CLAassistant commented Jul 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Implementation of ac_strategy.h and ac_strategy.c
For now JxlMemoryManager will be a wrapper around MemoryPool<T>.
Implementation of image.h and image.c; AC strategy implementation was slightly adjusted to reduce errors.
This is an implementation of field_encodings.h.

Note that I avoided implementing EnumValid() and Values() functions, as we have dedicated methods in .NET to do exactly that (Enum.IsDefined, Enum.GetValues)
Implementation of spline.h
Implemented ANS constants
@winscripter

Copy link
Copy Markdown
Author

While I'm working on this, I'd like to note something important.

Libjxl is licensed under the BSD 3-Clause license, and since I'm using libjxl code as reference, that means the license must be included.

I'm not really sure what would be the proper way to include the license. I might place the LICENSE.txt file in the Jxl folder or add a README linking to the libjxl repo.

Comment thread src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs Outdated
It is too large for a struct.
Add JxlAnsEntry and JxlAnsSymbol.

See ans_common.h. These correspond to the Entry and Symbol structures within AliasTable.
Comment thread src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs Outdated
Currently, there's a VarLenUint8/VarLenUint16 as well as histogram parsing implementation.

I will additionally have to implement parsing of ANS codes, uint config and LZ77 parameters.
@winscripter

Copy link
Copy Markdown
Author

I had originally planned to add a managed Brotli codec under the Compression folder. However, I discovered that .NET has shipped built-in Brotli support since 2018, so a custom implementation is unnecessary. The decoder now uses System.IO.Compression.BrotliStream, as shown here.

- Use Stream for Box Content Decoder
- Reduce errors in JxlDecoderCore
- Add GetStride method to JxlFrameDecoder
- Add JxlDctQuantWeightParameters and JxlQuantizerEncoding to implement more quant_weights.h components, and add proper documentation to each member of JxlQuantMode.
- Remove InlineArray2<T> (there's already one built into System.Runtime.CompilerServices, so prefer to use that)
Comment thread src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs Outdated
}
}

Span<byte> seen = stackalloc byte[index + 1];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's the debug guard in L864 above, but how big can references.Length become?

Besides stack allocating to a length which is a power of two, then slicing is better, so e.g. Span<byte> seen = stackalloc byte[128].Slice(index + 1).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Length of references seems to be arbitrary. It's a Span view over frameReferences (on L304) which doesn't have a fixed upper bound.

That being said, I do believe using an ArrayPool here might be necessary, as for really large images the seen Span could become very large and lead to a stack overflow.

Comment thread src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs Outdated
Common/Helpers

- Add InterleaveLower and InterleaveUpper to Vector128_ and Vector256_
- Add unit test for InterleaveLower and InterleaveUpper (specifically for Vector256_)
- Add Average to Numerics.cs

Common

- Add 32 and 33 to the InlineArray.tt text template

Formats/Jxl/IO/Metadata

- Remove unnecessary System.Runtime.CompilerServices using directive from JxlCustomTransformData and JxlOpsinInvreseMatrix

Formats/Jxl/Processing/Decoder

- Remove unncessary using SixLabors.ImageSharp.Formats.Jxl.IO

Formats/Jxl/Processing/Encoder

- Add partial Fast Lossless Encoder work (+enc_fast_lossless.cc; largest file in libjxl source)
- Add linear algebra (+enc_linalg.cc, +enc_linalg.h)

Formats/Jxl/Processing/Jpeg

- Work that would later become JXL<->JPEG lossless coding mode

Formats/Jxl/Processing/Modular/Encoding/ContextPrediction

- Finish context prediction (+context_predict.h)

Formats/Jxl/Processing/Modular/Transforms

- Finish Reversible Color Transform (+rct.cc, +rct.h, +enc_rct.cc, +enc_rct.h)
- Finish Palette/Indexed coding (+palette.cc, +palette.h, +enc_palette.cc, enc_palette.h)
- Finish Squeeze transform (+squeeze.cc, +squeeze.h, +enc_squeeze.cc, +enc_squeeze.h)

Formats/Jxl/Processing/RenderPipeline

- Incomplete render pipeline abstractions with EPF (Edge Preserving Filter) 0 stage (+render_pipeline_stage.cc, +render_pipeline_stage.h, +stage_epf.cc, +stage_epf.h)

Formats/Jxl/Processing/Splines

- Remove unnecessary System.Runtime.CompilerServices using directive

Formats/Jxl/Processing

- Add dequantizer matrices
- Remove JxlEndianness (prefer ByteOrder from ImageSharp/Common)
- Add missing constant to JxlLoopFilter
- Remove unnecessary using SixLabors.ImageSharp.Common.Helpers from JxlMath
- Replace JxlPixelFormat to use ByteOrder
- Update quantizers to use dequantizer matrices and quantizer weights
- Add quantizer encoding and constants
- Add SIMD utilities
- Remove System.Runtime.CompilerServices using from JxlWeightsSeparable5
- Remove InlineArray3, InlineArray36 and InlineArray15 from InlineArrays (3 and 15 already exist in System.Runtime.CompilerServices; 36 already exists in InlineArray.tt from ImageSharp/Common)

NEXT STEPS

The current focus would be applying refactors and optimizations from reviews, followed by completing the JPEG XL modular.
This avoids the stack cookie
Comment thread src/ImageSharp/Common/Helpers/Numerics.cs Outdated
Comment thread src/ImageSharp/Common/InlineArray.cs
Comment thread src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs
/// <summary>
/// Quantization values
/// </summary>
public InlineArray64<int> Values;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct will be quite big with > 256 bytes in size. Maybe a class is better here (for the JpegQuantizationTable). Depends on the usage though, at least copying the struct should be avoided.

Comment thread src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs Outdated
}
}

public static void ComputeMinMax(JxlModularChannel channel, out int min, out int max)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be vectorized? Would there be any benefit?
(of course could be done in a follow up PR)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think there would be benefit.

TensorPrimitives uses SIMD internally, so I think we can do the following:

public static void ComputeMinMax(JxlModularChannel channel, out int min, out int max)
{
    // Start with opposite bounds so the first iteration
    // guarantees to set these values
    min = int.MaxValue;
    max = int.MinValue;

    for (int y = 0; y < channel.Height; y++)
    {
        ReadOnlySpan<int> p = channel.GetRow(y);

        int minRow = TensorPrimitives.Min(p);
        int maxRow = TensorPrimitives.Max(p);

        min = Math.Min(minRow, min);
        max = Math.Max(maxRow, max);
    }
}

What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a sense how large channel.Width / p.Length can get. When it's in the tenth, then the approach with TensorPrimitives is really nice. When it can get bigger then that approach will be $O(2n)$ (once for min and once for max), so it should be combined into just having one iteration over each row.

Comment thread src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs
Comment thread src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs Outdated
This isn't a decoder or encoder. It's just a parser/writer so the codec can take some quantization parameters from a JPEG file, as JPEG and JPEG XL are very similar.
}

int len = jpegData.AppData[i].Count - 17;
if (iccPos + len > icc.Length)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit paranoid about overflow, so

if (iccPos > icc.Length - len)

to be on the safe side. Perf-wise there's no difference.

info.NumberOfAppMarkers++;
}

if (marker == 0xfe)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ifs below could be else if or a switch.
Right now every if is checked which is not necessary?

Comment on lines +923 to +924
acOk[0] = acOk[1] = acOk[2] = acOk[3] = false;
dcOk[0] = dcOk[1] = dcOk[2] = dcOk[3] = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed. By default above they're already initialized to false (part of the C# language spec).

/// <summary>
/// Start of spectral band in zigzag sequence
/// </summary>
public int Ss;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be properties here in that class.

/// Abstracts access to a raster frame data required for encoding.
/// </summary>
internal interface IFjxlFrameInputSource : IDisposable
internal abstract class IFjxlFrameInputSource : IDisposable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-IFjxlFrameInputSource 
+FjxlFrameInputSource 

For the naming assuming the I is the prefix for interface. But when the name in the reference source is with leading I it should be kept here.

private const int MaxFirstPreviewSize = 8;

/// <summary>
/// Computes the average of two integers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please append a hint that this average is specific to JPEG XL usage.

if (x < y)
{
RuntimeUtility.Swap(ref x, ref y);
(y, x) = (x, y);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the swap I don't know how "generic" the RuntimeUtility.Swap(ref x, ref y); is.
That swap with the value tuples I just in the quick prototype for Hypot because I was too lazy to bring in the aforementioned method.

I'm fine with either approach here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants