Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/ImageSharp/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -226,6 +227,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// <see cref="JxlConfigurationModule"/>.
/// <see cref="HeifConfigurationModule"/>.
/// <see cref="AniConfigurationModule"/>.
/// </summary>
Expand All @@ -241,6 +243,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new JxlConfigurationModule(),
new HeifConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule(),
Expand Down
18 changes: 18 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlConfigurationModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// Registers the JPEG XL encoders, decoders and mime type detectors.
/// </summary>
public sealed class JxlConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetDecoder(JxlFormat.Instance, JxlDecoder.Instance);
configuration.ImageFormatsManager.SetEncoder(JxlFormat.Instance, new JxlEncoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new JxlImageFormatDetector());
}
}
25 changes: 25 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Text;

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// Constants for the JPEG XL (JXL) format.
/// </summary>
internal static class JxlConstants
{
/// <summary>Gets the JPEG XL codestream signature (FF 0A).</summary>
public static ReadOnlySpan<byte> CodestreamMagic => new byte[] { 0xFF, 0x0A };

/// <summary>Gets the JPEG XL container signature.</summary>
public static ReadOnlySpan<byte> ContainerMagic => new byte[]
{ 0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A };

/// <summary>Gets the list of mimetypes that equate to a JXL.</summary>
public static string[] MimeTypes { get; } = ["image/jxl"];

/// <summary>Gets the list of file extensions that equate to a JXL.</summary>
public static string[] FileExtensions { get; } = ["jxl"];
}
60 changes: 60 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// Image decoder for JPEG XL images, decoded via the external libjxl
/// decoder (djxl) to PNG and then with the managed PNG decoder.
/// </summary>
internal class JxlDecoder : ImageDecoder
{
private JxlDecoder()
{
}

public static JxlDecoder Instance { get; } = new();

/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(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<TPixel> image = Image.Load<TPixel>(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);
}
}

/// <inheritdoc />
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(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);
}
}
58 changes: 58 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// Image encoder for writing an image as JPEG XL, encoded via the
/// external libjxl encoder (cjxl) from a managed PNG intermediate.
/// </summary>
public class JxlEncoder : ImageEncoder
{
/// <summary>
/// Gets the encoder effort (1-10, default 7) passed to cjxl.
/// </summary>
public int Effort { get; init; } = 7;

/// <summary>
/// Gets the Butteraugli distance (default 1.0, ignored when lossless).
/// </summary>
public float Distance { get; init; } = 1.0f;

/// <summary>
/// Gets a value indicating whether to encode losslessly (Modular mode).
/// </summary>
public bool Lossless { get; init; }

/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> 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);
}
}
}
32 changes: 32 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// Registers the JPEG XL format.
/// </summary>
public sealed class JxlFormat : IImageFormat<JxlMetadata>
{
private JxlFormat()
{
}

/// <summary>Gets the shared instance.</summary>
public static JxlFormat Instance { get; } = new();

/// <inheritdoc/>
public string DefaultMimeType => "image/jxl";

/// <inheritdoc/>
public string Name => "JXL";

/// <inheritdoc/>
public IEnumerable<string> MimeTypes => JxlConstants.MimeTypes;

/// <inheritdoc/>
public IEnumerable<string> FileExtensions => JxlConstants.FileExtensions;

/// <inheritdoc/>
public JxlMetadata CreateDefaultFormatMetadata() => new();
}
29 changes: 29 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Diagnostics.CodeAnalysis;

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// Detects JPEG XL file headers: the codestream signature (FF 0A) or the
/// 12-byte container signature.
/// </summary>
public class JxlImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 12;

/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? JxlFormat.Instance : null;
return format != null;
}

private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= 2
&& (JxlConstants.CodestreamMagic.SequenceEqual(header[..2])
|| (header.Length >= this.HeaderSize
&& JxlConstants.ContainerMagic.SequenceEqual(header[..12])));
}
63 changes: 63 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlMetadata.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides JPEG XL specific metadata information for the image.
/// </summary>
public class JxlMetadata : IFormatMetadata<JxlMetadata>
{
/// <summary>Initializes a new instance of the <see cref="JxlMetadata"/> class.</summary>
public JxlMetadata()
{
}

private JxlMetadata(JxlMetadata other) => this.HasAlpha = other.HasAlpha;

/// <summary>Gets or sets a value indicating whether the image has an alpha channel.</summary>
public bool HasAlpha { get; set; }

/// <inheritdoc/>
public static JxlMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
=> new() { HasAlpha = metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.Alpha) };

/// <inheritdoc/>
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),
};
}

/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new() { PixelTypeInfo = this.GetPixelTypeInfo() };

/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}

/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

/// <inheritdoc/>
public JxlMetadata DeepClone() => new(this);
}
46 changes: 46 additions & 0 deletions src/ImageSharp/Formats/Jxl/JxlTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Diagnostics;

namespace SixLabors.ImageSharp.Formats.Jxl;

/// <summary>
/// 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.
/// </summary>
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;
}
}
36 changes: 36 additions & 0 deletions tests/ImageSharp.Tests/Formats/Jxl/JxlImageFormatDetectorTests.cs
Original file line number Diff line number Diff line change
@@ -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 _));
}
}