Skip to content

Support for JPEG XL (JXL) images - #3153

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

Support for JPEG XL (JXL) images#3153
winscripter wants to merge 136 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.
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
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.
Comment thread src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs
Comment thread src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs Outdated
…ise encoder tools, and complete render pipeline EPF 0 stage

Files implemented:
- enc_gaborish.cc
- enc_gaborish.h
- enc_gamma_correct.h
- enc_huffman.cc
- enc_huffman.h
- enc_huffman_tree.cc
- enc_huffman_tree.h
- enc_noise.cc
- enc_noise.h
- render_pipeline/stage_epf.cc
- render_pipeline/stage_epf.h
1. Reorder the overflow check in JpegData
2. Prefer switch in JpegData instead of multiple if statements
3. Don't explicitly false initialize acOk and dcOk in JpegData (they're already initialized to false).
4. IFjxlFrameInputSource -> FjxlFrameInputSource (it's an abstract class, I prefix is for interfaces)
5. Document that the JxlSqueeze.Average method is specific to the squeeze transform.
6. Prefer RuntimeUtility.Swap over tuple-based swap (micro-optimization)
Comment thread src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs Outdated
Comment thread src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs Outdated
public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v, bool invert = false)
{
float epsilon = 1e-2f;
v = Math.Max(0, v); // cannot be < 0

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.

Instead the comment use a debug guard?

@winscripter winscripter Sep 3, 2026

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.

The Math.Max call limits the v variable so it can't be below 0 (and if it is, it gets set to 0), the comment just explains what that line does.

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.

Ey shame on me that I didn't read it this way. W/o the comment it would be more obvious (at least for me), so I'd drop the comment here.

continue;
}

Vector256<float> vsm = Vector256.Create((ReadOnlySpan<float>)sadMul[ix..]);

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.

What's when Vector256 isn't supported?
When it's guarded / checked at the call-site, then add a debug guard in this method to claim that Vec256 is supported.

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.

EPF stages operate at a fixed-size lane of 8 floats per iteration (in reference), and I decided to use Vector256 for this.

That being said, if the CPU doesn't support 256-bit vectors, it shouldn't fail - the JIT will just separate it into halves (if 128bit vectors supported), quarters (if 64bit vectors supported), or scalar (when no SIMD support). For example, my CPU (x86-64) supports vector instruction sets up to AVX2 (which is 256-bit), yet I can still use 512-bit vectors just fine.

C# interactive

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.

Yep, and the JIT may emit software fallback code (which is in it's nature slower).
In order to keep the code clean, your approach is fine.
For tests / CI care has to be taken that these code pathes are also executed when no HW-acceleration is available, etc.
Further an explicit code path for Vec128 (for arm and wasm) can be added later on (if benchmarks show it's necessary).

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.

I think I can use Vector<T> here actually, with minor changes. I'll just need unit tests to ensure that changing it doesn't break stuff, which we don't have yet (the codec isn't ready for tests at the moment). I'll look into using Vector<T> instead of Vector256 when we have working JPEG XL tests set up.

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.

Sounds good 👍🏻

ref Vector256<float> B,
ref Vector256<float> w)
{
Vector256<float> cx = Vector256.Create((ReadOnlySpan<float>)rows[0][1 + row][x..].Span);

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.

Ditto.

{
int channels = 3 + extraChannelInfos.Count;

Span<Memory<float>> rowPtrs = new Memory<float>[channels];

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.

Can the array allocation be avoided?

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.

Unfortunately I wasn't able to remove the array allocation. Trying to stackalloc a Memory gives error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type ('Memory<float>').

That being said, the allocation should be very small - should be < 10 items.

@winscripter winscripter Sep 3, 2026

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.

Actually, I'll consider using an InlineArray10 with a separate length variable once I implement AddOneRow to JxlPatchDictionary. That should remove the allocation. 💡


namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline;

internal sealed class WriteToOutputStage

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.

Is there something missing?
All the members are private and so unused at the moment.

Maybe this comment is just a reminder 😉.

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.

Yup, it's incomplete.

The write to output stage in reference had a lot of pointer-heavy code which I found more difficult to port to managed C# compared to other stages. So I implemented a few simple methods and saved this progress for later.

Probably will get back to it today.

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