diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs
index 47a54e7480..9a2243b4be 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -13,6 +13,7 @@
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Jxl;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
@@ -226,6 +227,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
/// .
/// .
/// .
+ /// .
/// .
/// .
///
@@ -241,6 +243,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
+ new JxlConfigurationModule(),
new HeifConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule(),
diff --git a/src/ImageSharp/Formats/Jxl/JxlConfigurationModule.cs b/src/ImageSharp/Formats/Jxl/JxlConfigurationModule.cs
new file mode 100644
index 0000000000..03f94750f0
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlConfigurationModule.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Registers the JPEG XL encoders, decoders and mime type detectors.
+///
+public sealed class JxlConfigurationModule : IImageFormatConfigurationModule
+{
+ ///
+ public void Configure(Configuration configuration)
+ {
+ configuration.ImageFormatsManager.SetDecoder(JxlFormat.Instance, JxlDecoder.Instance);
+ configuration.ImageFormatsManager.SetEncoder(JxlFormat.Instance, new JxlEncoder());
+ configuration.ImageFormatsManager.AddImageFormatDetector(new JxlImageFormatDetector());
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlConstants.cs b/src/ImageSharp/Formats/Jxl/JxlConstants.cs
new file mode 100644
index 0000000000..20e3ca5a28
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlConstants.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Text;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Constants for the JPEG XL (JXL) format.
+///
+internal static class JxlConstants
+{
+ /// Gets the JPEG XL codestream signature (FF 0A).
+ public static ReadOnlySpan CodestreamMagic => new byte[] { 0xFF, 0x0A };
+
+ /// Gets the JPEG XL container signature.
+ public static ReadOnlySpan ContainerMagic => new byte[]
+ { 0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A };
+
+ /// Gets the list of mimetypes that equate to a JXL.
+ public static string[] MimeTypes { get; } = ["image/jxl"];
+
+ /// Gets the list of file extensions that equate to a JXL.
+ public static string[] FileExtensions { get; } = ["jxl"];
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlDecoder.cs b/src/ImageSharp/Formats/Jxl/JxlDecoder.cs
new file mode 100644
index 0000000000..8d333540e2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlDecoder.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Image decoder for JPEG XL images, decoded via the external libjxl
+/// decoder (djxl) to PNG and then with the managed PNG decoder.
+///
+internal class JxlDecoder : ImageDecoder
+{
+ private JxlDecoder()
+ {
+ }
+
+ public static JxlDecoder Instance { get; } = new();
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ string dir = JxlTool.NewTempDir();
+ try
+ {
+ string jxlPath = Path.Combine(dir, "image.jxl");
+ string pngPath = Path.Combine(dir, "image.png");
+ using (var file = new FileStream(jxlPath, FileMode.Create, FileAccess.Write))
+ {
+ stream.CopyTo(file);
+ }
+
+ JxlTool.Run("djxl", jxlPath, pngPath);
+ Image image = Image.Load(pngPath);
+ ScaleToTargetSize(options, image);
+ return image;
+ }
+ catch (System.ComponentModel.Win32Exception e)
+ {
+ throw new ImageFormatException("JPEG XL decoding requires the libjxl 'djxl' tool on PATH.", e);
+ }
+ finally
+ {
+ Directory.Delete(dir, recursive: true);
+ }
+ }
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ => this.Decode(options, stream, cancellationToken);
+
+ protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ using Image image = this.Decode(options, stream, cancellationToken);
+ return new ImageInfo(new Size(image.Width, image.Height), image.Metadata);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlEncoder.cs b/src/ImageSharp/Formats/Jxl/JxlEncoder.cs
new file mode 100644
index 0000000000..ab1549468d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlEncoder.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Image encoder for writing an image as JPEG XL, encoded via the
+/// external libjxl encoder (cjxl) from a managed PNG intermediate.
+///
+public class JxlEncoder : ImageEncoder
+{
+ ///
+ /// Gets the encoder effort (1-10, default 7) passed to cjxl.
+ ///
+ public int Effort { get; init; } = 7;
+
+ ///
+ /// Gets the Butteraugli distance (default 1.0, ignored when lossless).
+ ///
+ public float Distance { get; init; } = 1.0f;
+
+ ///
+ /// Gets a value indicating whether to encode losslessly (Modular mode).
+ ///
+ public bool Lossless { get; init; }
+
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ string dir = JxlTool.NewTempDir();
+ try
+ {
+ string pngPath = Path.Combine(dir, "image.png");
+ string jxlPath = Path.Combine(dir, "image.jxl");
+ image.Save(pngPath);
+ try
+ {
+ JxlTool.Run(
+ "cjxl",
+ pngPath,
+ jxlPath,
+ $"--effort={this.Effort}",
+ this.Lossless ? "--lossless=1" : $"--distance={this.Distance.ToString(System.Globalization.CultureInfo.InvariantCulture)}");
+ }
+ catch (System.ComponentModel.Win32Exception e)
+ {
+ throw new ImageFormatException("JPEG XL encoding requires the libjxl 'cjxl' tool on PATH.", e);
+ }
+
+ using var file = new FileStream(jxlPath, FileMode.Open, FileAccess.Read);
+ file.CopyTo(stream);
+ }
+ finally
+ {
+ Directory.Delete(dir, recursive: true);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs
new file mode 100644
index 0000000000..04bfcdb117
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Registers the JPEG XL format.
+///
+public sealed class JxlFormat : IImageFormat
+{
+ private JxlFormat()
+ {
+ }
+
+ /// Gets the shared instance.
+ public static JxlFormat Instance { get; } = new();
+
+ ///
+ public string DefaultMimeType => "image/jxl";
+
+ ///
+ public string Name => "JXL";
+
+ ///
+ public IEnumerable MimeTypes => JxlConstants.MimeTypes;
+
+ ///
+ public IEnumerable FileExtensions => JxlConstants.FileExtensions;
+
+ ///
+ public JxlMetadata CreateDefaultFormatMetadata() => new();
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs
new file mode 100644
index 0000000000..1c15d05b15
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Detects JPEG XL file headers: the codestream signature (FF 0A) or the
+/// 12-byte container signature.
+///
+public class JxlImageFormatDetector : IImageFormatDetector
+{
+ ///
+ public int HeaderSize => 12;
+
+ ///
+ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format)
+ {
+ format = this.IsSupportedFileFormat(header) ? JxlFormat.Instance : null;
+ return format != null;
+ }
+
+ private bool IsSupportedFileFormat(ReadOnlySpan header)
+ => header.Length >= 2
+ && (JxlConstants.CodestreamMagic.SequenceEqual(header[..2])
+ || (header.Length >= this.HeaderSize
+ && JxlConstants.ContainerMagic.SequenceEqual(header[..12])));
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlMetadata.cs b/src/ImageSharp/Formats/Jxl/JxlMetadata.cs
new file mode 100644
index 0000000000..f1b83683bf
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlMetadata.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Provides JPEG XL specific metadata information for the image.
+///
+public class JxlMetadata : IFormatMetadata
+{
+ /// Initializes a new instance of the class.
+ public JxlMetadata()
+ {
+ }
+
+ private JxlMetadata(JxlMetadata other) => this.HasAlpha = other.HasAlpha;
+
+ /// Gets or sets a value indicating whether the image has an alpha channel.
+ public bool HasAlpha { get; set; }
+
+ ///
+ public static JxlMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
+ => new() { HasAlpha = metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.Alpha) };
+
+ ///
+ public PixelTypeInfo GetPixelTypeInfo()
+ {
+ if (this.HasAlpha)
+ {
+ return new PixelTypeInfo(32)
+ {
+ AlphaRepresentation = PixelAlphaRepresentation.Unassociated,
+ ColorType = PixelColorType.RGB | PixelColorType.Alpha,
+ ComponentInfo = PixelComponentInfo.Create(4, 32, 8, 8, 8, 8),
+ };
+ }
+
+ return new PixelTypeInfo(24)
+ {
+ ColorType = PixelColorType.RGB,
+ ComponentInfo = PixelComponentInfo.Create(3, 24, 8, 8, 8),
+ };
+ }
+
+ ///
+ public FormatConnectingMetadata ToFormatConnectingMetadata()
+ => new() { PixelTypeInfo = this.GetPixelTypeInfo() };
+
+ ///
+ public void AfterImageApply(Image destination, Matrix4x4 matrix)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+
+ ///
+ public JxlMetadata DeepClone() => new(this);
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlTool.cs b/src/ImageSharp/Formats/Jxl/JxlTool.cs
new file mode 100644
index 0000000000..035417c90b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlTool.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+///
+/// Runs an external libjxl tool (cjxl/djxl). JPEG XL encoding in this
+/// format is delegated to libjxl because ImageSharp has no managed JXL
+/// codec yet; only cjxl can also transcode legacy JPEG bit-exactly.
+///
+internal static class JxlTool
+{
+ public static void Run(string tool, params string[] args)
+ {
+ var startInfo = new ProcessStartInfo(tool)
+ {
+ CreateNoWindow = true,
+ RedirectStandardError = true,
+ };
+ foreach (string arg in args)
+ {
+ startInfo.ArgumentList.Add(arg);
+ }
+
+ using Process? process = Process.Start(startInfo);
+ if (process is null)
+ {
+ throw new ImageFormatException($"Failed to start '{tool}'. Install libjxl (cjxl/djxl) to use JPEG XL.");
+ }
+
+ process.WaitForExit();
+ if (process.ExitCode != 0)
+ {
+ throw new ImageFormatException($"'{tool}' failed with exit code {process.ExitCode}: {process.StandardError.ReadToEnd()}");
+ }
+ }
+
+ public static string NewTempDir()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), "imagesharp-jxl-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ return dir;
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Jxl/JxlImageFormatDetectorTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/JxlImageFormatDetectorTests.cs
new file mode 100644
index 0000000000..10ee980573
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Jxl/JxlImageFormatDetectorTests.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Jxl;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Jxl;
+
+public class JxlImageFormatDetectorTests
+{
+ [Fact]
+ public void DetectCodestreamSignature()
+ {
+ var detector = new JxlImageFormatDetector();
+ byte[] header = [0xFF, 0x0A, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ Assert.True(detector.TryDetectFormat(header.AsSpan(0, detector.HeaderSize), out IImageFormat format));
+ Assert.Equal(JxlFormat.Instance, format);
+ }
+
+ [Fact]
+ public void DetectContainerSignature()
+ {
+ var detector = new JxlImageFormatDetector();
+ byte[] header = [0x00, 0x00, 0x00, 0x0C, (byte)'J', (byte)'X', (byte)'L', (byte)' ', 0x0D, 0x0A, 0x87, 0x0A];
+ Assert.True(detector.TryDetectFormat(header.AsSpan(0, detector.HeaderSize), out IImageFormat format));
+ Assert.Equal(JxlFormat.Instance, format);
+ }
+
+ [Fact]
+ public void RejectJpegSignature()
+ {
+ var detector = new JxlImageFormatDetector();
+ byte[] header = [0xFF, 0xD8, 0xFF, 0xE0, 0, 0, 0, 0, 0, 0, 0, 0];
+ Assert.False(detector.TryDetectFormat(header.AsSpan(0, detector.HeaderSize), out _));
+ }
+}