Skip to content

Comments

feat: add JPEG XL (JXL) encoding/decoding support#8

Merged
clroot merged 12 commits intomainfrom
feat/jxl-encoding
Feb 19, 2026
Merged

feat: add JPEG XL (JXL) encoding/decoding support#8
clroot merged 12 commits intomainfrom
feat/jxl-encoding

Conversation

@clroot
Copy link
Owner

@clroot clroot commented Feb 19, 2026

Summary

  • Add JPEG XL encoding and decoding via libjxl FFI bindings
  • Implement slimg-libjxl-sys crate with prebuilt binary download support for crates.io publishing
  • Add safe Rust encoder/decoder wrappers in slimg-core
  • Add JXL format option to CLI

Key architecture decisions

  • Prebuilt binary download: libjxl submodule (~95MB) is excluded from the crates.io package. Instead, prebuilt static libraries are downloaded from GitHub Releases at build time (package size: 29KB compressed)
  • Smart fallback: default = ["vendored"] builds from source when the submodule is present (local dev), falls back to prebuilt download when source is absent (crates.io consumers)
  • CI workflow: build-libjxl-prebuilt.yml builds static libraries for 5 platforms (linux-x86_64, linux-aarch64, macos-x86_64, macos-aarch64, windows-x86_64)

Test plan

  • cargo build -p slimg-libjxl-sys — vendored build passes
  • cargo build -p slimg-core — downstream build passes
  • cargo test -p slimg-libjxl-sys -p slimg-core — all tests pass
  • cargo package -p slimg-libjxl-sys --no-default-features --no-verify — 29KB package
  • Run build-libjxl-prebuilt.yml workflow to generate prebuilt archives
  • Verify prebuilt download path works end-to-end

🤖 Generated with Claude Code

Clean-room libjxl (BSD-3-Clause) binding approach for JXL encoding
support, independent of GPL-licensed jpegxl-sys/jpegxl-rs.
9 tasks covering libjxl submodule setup, sys crate build,
safe wrapper, Codec trait integration, and CI updates.
- Rename crate from libjxl-enc-sys to libjxl-sys (now provides both
  encoder and decoder bindings)
- Add libjxl decoder bindings (JxlDecoder*) to build.rs bindgen
- Create decoder.rs safe wrapper using libjxl event-based decode API
- Rewrite JxlCodec to use libjxl for both encode and decode (replacing
  image crate's limited JXL decode support)
- Add roundtrip tests (lossy + lossless) that verify encode→decode
- Update pipeline test to expect JXL encoding success
- Update format.rs can_encode to return true for all formats
- Add cmake to dist-workspace.toml system dependencies (homebrew,
  apt, chocolatey) for cargo-dist release builds
- Add submodules: recursive to kotlin/python bindings workflows
- Add cmake to system dependencies install steps in both workflows
- Add crates/libjxl-sys/** to path triggers for bindings workflows
JXL encoding is now supported, so add it to FormatArg enum
to allow --format jxl in convert, resize, crop, extend commands.
- Rename crate from `libjxl-sys` to `slimg-libjxl-sys` (existing name taken on crates.io)
- Remove `publish = false`, add repository/homepage metadata
- Use `package = "slimg-libjxl-sys"` alias in slimg-core to keep `libjxl_sys` import name
- Add slimg-libjxl-sys publish step to publish.yml (before slimg-core)

Note: crates.io 10MB limit prevents vendoring libjxl source directly.
Prebuilt binary download approach (build.rs) is planned as next step.
Solve the crates.io 10MB package limit by downloading prebuilt static
libraries from GitHub Releases instead of including the ~95MB libjxl
submodule source.

Build paths (in priority order):
1. LIBJXL_SYS_DIR env var — user-provided prebuilt directory
2. vendored feature — cmake source build (if submodule present)
3. prebuilt download — curl from GitHub Releases (crates.io default)

Also add build-libjxl-prebuilt.yml CI workflow for building prebuilt
archives across 5 platforms, and update publish.yml to use
--no-default-features for the slimg-libjxl-sys crate.

Remove separate skcms_transform compilation as the transform code is
now integrated into skcms.cc (included in jxl_cms).
@gemini-code-assist
Copy link

Summary of Changes

Hello @clroot, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the image processing capabilities by introducing full support for the JPEG XL format. It involves creating a new FFI crate to interface with the libjxl C library, complete with a sophisticated build process that prioritizes prebuilt binaries for distribution while allowing source compilation for development. The core image processing logic now includes safe Rust wrappers for JXL encoding and decoding, and the CLI has been updated to expose this new functionality to users. This change broadens the utility of the application by supporting a modern, efficient image format.

Highlights

  • JPEG XL (JXL) Support: Added comprehensive encoding and decoding support for JPEG XL images, integrating the libjxl library.
  • New slimg-libjxl-sys Crate: Introduced a new Rust crate to provide FFI bindings for libjxl, supporting both vendored builds from source and downloading prebuilt static libraries for smaller package sizes on crates.io.
  • Safe Rust Wrappers: Implemented safe Rust encoder and decoder wrappers within slimg-core for JXL, ensuring robust and idiomatic usage of the underlying C library.
  • CLI Integration: Extended the command-line interface (CLI) to include JXL as a supported format for image conversion operations.
  • Optimized Build Process: Designed a build system that intelligently falls back to prebuilt binaries when the libjxl submodule is not present, significantly reducing package size for consumers, and established a CI workflow for generating these prebuilt artifacts across multiple platforms.
Changelog
  • .gitmodules
    • Added libjxl as a git submodule within crates/libjxl-sys.
  • Cargo.lock
    • Updated dependencies to include bindgen, cexpr, clang-sys, cmake, libloading, and slimg-libjxl-sys.
  • Cargo.toml
    • Added crates/libjxl-sys to the workspace members.
  • cli/src/commands/mod.rs
    • Updated FormatArg enum to include Jxl.
    • Modified into_format implementation to map FormatArg::Jxl to Format::Jxl.
  • crates/libjxl-sys/Cargo.toml
    • Added new crate manifest for slimg-libjxl-sys.
    • Defined vendored feature and build dependencies for cmake and bindgen.
  • crates/libjxl-sys/build.rs
    • Added build script to handle vendored libjxl compilation via CMake and bindgen.
    • Implemented logic for downloading prebuilt libjxl binaries from GitHub releases.
    • Included platform detection and linking instructions for static libraries.
  • crates/libjxl-sys/libjxl
    • Added a reference to the libjxl git submodule at a specific commit.
  • crates/libjxl-sys/src/lib.rs
    • Added main library file to include generated FFI bindings from build.rs.
  • crates/slimg-core/Cargo.toml
    • Added slimg-libjxl-sys as a dependency for slimg-core.
  • crates/slimg-core/src/codec/jxl.rs
    • Removed the previous decode-only JXL codec implementation.
  • crates/slimg-core/src/codec/jxl/decoder.rs
    • Added a new safe Rust wrapper for libjxl decoding functionality.
  • crates/slimg-core/src/codec/jxl/encoder.rs
    • Added a new safe Rust wrapper for libjxl encoding functionality.
  • crates/slimg-core/src/codec/jxl/mod.rs
    • Added the JXL codec module, integrating the new decoder and encoder.
    • Included unit tests for JXL encoding and decoding roundtrips, including lossless and lossy scenarios.
  • crates/slimg-core/src/codec/jxl/types.rs
    • Added JXL encoding configuration types, including lossless and distance parameters.
  • crates/slimg-core/src/format.rs
    • Updated can_encode method to return true for JXL, removing the previous license-based restriction.
    • Modified related unit tests to reflect JXL encoding support.
  • crates/slimg-core/src/pipeline.rs
    • Updated the jxl_encode_returns_error test to jxl_encode_succeeds to reflect the new JXL encoding capability.
  • dist-workspace.toml
    • Added cmake as a system dependency for homebrew, apt, and chocolatey build stages.
  • docs/plans/2026-02-19-jxl-encoding-design.md
    • Added a detailed design document outlining the plan for JPEG XL encoding implementation.
Ignored Files
  • Ignored by pattern: .github/workflows/** (4)
    • .github/workflows/build-libjxl-prebuilt.yml
    • .github/workflows/kotlin-bindings.yml
    • .github/workflows/publish.yml
    • .github/workflows/python-bindings.yml
Activity
  • The author (clroot) has implemented and tested the new JPEG XL encoding and decoding features.
  • Tests were performed to ensure vendored builds pass, downstream builds pass, all unit tests pass, and the slimg-libjxl-sys package size is optimized.
  • The author also planned to run the build-libjxl-prebuilt.yml workflow and verify the prebuilt download path end-to-end.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces JPEG XL (JXL) encoding and decoding capabilities, a significant feature addition. The implementation utilizes a new slimg-libjxl-sys crate for handling vendored source builds and pre-built binary downloads, with robust safe Rust wrappers for the libjxl FFI. However, several security concerns have been identified: the build script lacks checksum verification for downloaded binaries, posing a supply chain risk; the decoder is vulnerable to memory exhaustion DoS attacks from malicious image headers; and the encoder relies on debug-only assertions for critical buffer size validation. Additionally, there are suggestions to enhance the build script's robustness and a note regarding a potentially temporary documentation file. Addressing these points will further improve the security and overall quality of this excellent contribution.

Comment on lines +159 to +186
let status = Command::new("curl")
.args([
"--proto",
"=https",
"--tlsv1.2",
"-L",
"--fail",
"--silent",
"--show-error",
"-o",
])
.arg(&archive_path)
.arg(&url)
.status()
.unwrap_or_else(|e| {
panic!(
"slimg-libjxl-sys: failed to run curl: {e}\n\
Hint: install curl, or set LIBJXL_SYS_DIR, or enable the vendored feature"
)
});

assert!(
status.success(),
"slimg-libjxl-sys: failed to download prebuilt (HTTP error).\n\
URL: {url}\n\
Hint: check network connectivity, or set LIBJXL_SYS_DIR, or enable the vendored feature"
);

Choose a reason for hiding this comment

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

security-high high

The build script downloads prebuilt binaries from GitHub Releases but does not verify their integrity using a checksum (e.g., SHA-256). This exposes the build process to supply chain attacks if the download source or the network connection is compromised. Malicious binaries could be injected and executed during the build or at runtime.

return Err(Error::Decode("failed to get output buffer size".into()));
}

pixels.resize(buf_size, 0);

Choose a reason for hiding this comment

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

security-medium medium

The decoder allocates memory for the output buffer based on the dimensions reported in the JXL header (info.xsize and info.ysize) without any upper bound validation. A malicious JXL file claiming extremely large dimensions could trigger a massive allocation via pixels.resize(buf_size, 0), leading to a Denial of Service (DoS) through memory exhaustion (OOM).

};

let expected = (width as usize) * (height as usize) * 4;
debug_assert_eq!(pixels.len(), expected);

Choose a reason for hiding this comment

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

security-medium medium

The encoder uses debug_assert_eq! to validate that the input pixel buffer size matches the expected dimensions. Since debug_assert! is removed in release builds, this check is effectively bypassed in production. If the provided buffer is smaller than the dimensions specified to the encoder, it could lead to a buffer over-read during the FFI call to libjxl. This should be replaced with a robust check that returns an error.

        let expected = (width as usize) * (height as usize) * 4;
        if pixels.len() != expected {
            return Err(Error::Encode(format!("invalid pixel buffer size: expected {}, got {}", expected, pixels.len())));
        }

Comment on lines +78 to +87
.header(src_include.join("jxl/encode.h").to_str().unwrap())
.header(src_include.join("jxl/decode.h").to_str().unwrap())
.header(src_include.join("jxl/types.h").to_str().unwrap())
.header(
src_include
.join("jxl/codestream_header.h")
.to_str()
.unwrap(),
)
.header(src_include.join("jxl/color_encoding.h").to_str().unwrap())

Choose a reason for hiding this comment

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

medium

Using .unwrap() on the result of .to_str() can lead to a panic with a generic message if a path contains non-UTF-8 characters. While unlikely for these project-internal paths, it's more robust to use .expect() with a specific message to aid debugging in case of cross-platform build issues.

Comment on lines +193 to +194
.status()
.expect("slimg-libjxl-sys: failed to run tar");

Choose a reason for hiding this comment

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

medium

The error handling for the tar command is less descriptive than for curl. If tar is not found, the build will panic with a generic message. It would be better to provide a more helpful hint to the user, consistent with how a missing curl is handled.

Suggested change
.status()
.expect("slimg-libjxl-sys: failed to run tar");
.status()
.unwrap_or_else(|e| {
panic!(
"slimg-libjxl-sys: failed to run tar: {e}\n\
Hint: install tar, or set LIBJXL_SYS_DIR, or enable the vendored feature"
)
})

@@ -0,0 +1,792 @@
# JPEG XL Encoding Implementation Plan

Choose a reason for hiding this comment

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

medium

This file appears to be an AI-generated implementation plan. It's written in Korean, which is inconsistent with the English used elsewhere in the project, and contains AI-specific directives (e.g., "For Claude: ..."). Please consider if this file is intended as permanent project documentation. If it was a temporary planning aid, it might be better to remove it before merging to keep the repository clean and consistent.

@clroot clroot merged commit bed880e into main Feb 19, 2026
6 of 15 checks passed
@clroot clroot deleted the feat/jxl-encoding branch February 19, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant