Skip to content

rsilvabr/jxl-photo

Repository files navigation

jxl_photo — JXL Workflow Manager

Batch JPEG XL conversion tools with full ICC color profile and EXIF metadata preservation. Designed for photographers working with 16-bit TIFF files who want compact JXL archives without losing color accuracy or metadata. Tested with Capture One, Lightroom, NX Studio, Photoshop, and Fuji Hyper Utility exported 16-bit TIFFs.


Why JPEG XL?

Spectacular compression with no compromise on bit depth.

  • Lossless 16-bit files much smaller than TIFF and TIFF with ZIP/Deflate
  • Lossy 16-bit files — small files that retain full 16-bit tonal information, something no other common format achieves (JPEG is 8-bit, TIFF lossless is large)
  • This is genuinely new: small lossy files, but with 16-bit color depth

Here is an example of the gains when using JXL with 45MP Nikon Z7 files:

Format Typical size (45MP, 16-bit)
TIFF 16-bit ~260 MB, ~245 MB (zip/deflate)
JXL 16-bit lossless ~173 MB
JXL 16-bit lossy d=0.05 ~47 MB
JXL 16-bit lossy d=0.1 ~34 MB
JXL 16-bit lossy d=1.0 (visually lossless) ~8 MB

I have tested with different settings and posted on reddit, click here and here to check.

What's New

Unreleased (in main)--run-preset NAME runs a saved preset without the menu and exits, so a recurring sync can live in Task Scheduler or cron. Sync is the default (--overwrite to redo everything) and --dry-run is never inherited from the stored run; presets that delete sources are refused unattended. --list-presets shows what is saved.

Current release: v1.8.3 (2026-07-28) — recommended for everyone. Cuts the repetitive parts of the menu (your own distance, repeatable manifests, named presets) and adds honest reporting for multi-folder runs on top of v1.8.2's three rounds of hardening: you can start a manifest, walk away, and find out on return whether any file broke — and which one.

Breaking change (inherited from v1.8.0): in jxl_jpeg_transcoder.py, modes 4 and 5 were swapped4 = folder rename, 5 = sibling folder. Swap them in saved commands and manifests.

Highlights:

  • Your own distance in the menu — entry [2] of Step 2 now shows the distance you set in option 4 (e.g. d=0.05 — Your default), instead of forcing a trip through [4] Custom on every single run. Ships as 0.1, so nothing changes until you set it. [4] Custom comes pre-filled with the last value you used.
  • Manifest runs are repeatableRepeat last workflow now handles them: it re-reads the CSV (so edits you made in Excel count), skips the pointless input-folder question and goes straight to overwrite/sync and dry-run. Keeping a library in sync is two keystrokes; it used to mean walking through the whole wizard again.
  • Presets (option 7) — save the last workflow under a name and run it later. Several recurring jobs (a nightly manifest sync, a per-shoot conversion) can coexist instead of overwriting one another in the single "last workflow" slot.
  • Settings that actually apply — changing workers/quality/effort/distance in option 4 now also drives the next run, repeats included. They used to write a separate value that no run ever read, so editing them appeared to do nothing.
  • Manifest runs end with a real summary — a per-folder table, a file-level TOTAL across every entry, and the paths of the files that failed. A multi-hour run over three folders used to end with 3 OK (counting folders), the per-folder numbers already scrolled away into three separate logs. Also saved to Logs/jxl_photo/<timestamp>.log.
  • Corrupt files no longer hide in the skipped count — a TIFF with no readable pages was reported as "skipped by multipage policy", a policy that never asked for it. It now has its own count and its own section. Exit codes are unchanged: a damaged input is not a failed run.
  • No silent data loss — 16-bit decode failures, a "repaired" corrupt JXL, and weak delete verification now all fail loudly instead of quietly proceeding.
  • Multi-page TIFFs are safe by default — every page is kept (--multipage-mode split) instead of just page 0, and mode 8 refuses to delete a source whose pages were dropped.
  • Every output is verified before being trusted — a corrupt or partial JXL/JPEG/PNG/TIFF is caught and cleaned up instead of reported OK.
  • Manifests are Excel-safe and collision-checked — UTF-8 BOM for non-ASCII paths, cross-entry output collisions refused up front.
  • Better throughput on large libraries — the thread pool no longer stalls at folder boundaries.

Full release notes: v1.8.3 · v1.8.2 · v1.8.1


Release history

Version Date Highlights
v1.8.3 2026-07-28 Configurable default distance in the menu, repeatable manifest runs, named presets, settings that reach the next run; manifest run summary: per-folder table, file-level totals, failed paths listed; corrupt files split out of the skipped count; combined log in Logs/jxl_photo/
v1.8.2 2026-07-27 Independent audit + real-batch fixes: ignored thumbnails no longer deleted, missing tools fail fast, multi-page default is now split, thread pool no longer stalls across folders
v1.8.1 2026-07-26 Audit release: data-safety hardening, multi-page reconstruction v2, integrity gates, manifest coverage guards
v1.8.0 2026-07-18 libjxl v0.12 support, output integrity verification, direction-restriction flags, transcoder modes 4/5 swapped
v1.7.2 2026-07-18 Wrapper delete-source confirmation unstuck; lossy convert keeps Exif/XMP before the codestream
v1.7.1 2026-07-13 Cautious ICC strategy (round-trip test + cache), .jfif/.jpe support
v1.7.0 2026-07-12 Multi-page TIFF support: split/skip/ignore, thumbnail handling, per-page ICC, marker-based reconstruction
v1.6.0 2026-07-05 Audit-driven fixes: staging concurrency, wrapper routing, manifest Mode column, CMYK rejection
v1.5.3 2026-04-15 Full Auto Mode, PNG bit depth, EXIF preservation, 8-bit TIFF black-image fix, stable
v1.4 2026-04-11 JXL → JPEG workflow: lossy/lossless conversion modes
v1.3 2026-04-11 Auto Mode (beta), manifest system, embedded JPEG thumbnail
v1.2 2026-04-05 Basic/None decode modes, ICC mode selector
v1.1 2026-04-05 D50 patch modes, metadata strip, race-condition fixes
v1.0 2026-04-02 First stable release — TIFF and JPEG → JXL with ICC preservation

Older "What's New" sections in full: docs/version_history.md


Features

1. TIFF → JXL Encoding

  • 16-bit TIFF preservation (lossless or near-lossless JXL)
  • ICC profile preservation — exact original ICC restored on round-trip, even for lossy JXL
  • EXIF/XMP metadata — fully preserved and visible in IrfanView, XnView MP, and other applications
  • JPEG preview embedding in output TIFF (fast Explorer thumbnails)

2. JXL → TIFF Decoding

  • Three decode modes: Roundtrip (ICC-restored), Basic (for consumer JXLs), Matrix (color space conversion)
  • JPEG preview embedding in output TIFF
  • Sync mode — reconvert only changed files

3. JPEG ↔ JXL Transcoding

  • JPEG → JXL lossless transcoding (pixel-perfect)
  • JXL → JPEG/PNG with ICC color space conversion (sRGB, AdobeRGB, ProPhoto RGB)
  • JPEG preview embedding

4. Professional Workflow Support

  • Multiple folder structure modes (flat, recursive, Capture One / Lightroom EXPORT workflows)
  • Parallel processing (tested up to 32 workers)
  • Sync mode (reconvert only changed files)
  • Staging SSD support for large collections

Scripts

Script Purpose Key Feature
jxl_photo.py Interactive wizard Guided workflow with Auto Mode — analyzes folders and recommends best mode automatically
jxl_tiff_encoder.py TIFF → JXL encoder Embeds ICC in XMP for round-trip preservation; multi-page TIFF splitting
jxl_tiff_decoder.py JXL → TIFF decoder Restores original ICC from XMP using Roundtrip Mode; reconstructs multi-page TIFFs
jxl_jpeg_transcoder.py JPEG ↔ JXL / JXL → PNG Lossless transcoding, ICC conversion, PNG output

Quick Start — Interactive Wrapper

The easiest way to use this toolkit. Run py jxl_photo.py and follow the guided menu:

╭───────────────────────────────────────────── JXL Tools Environment ────────────────────────────────────────────────╮
│ [✓] cjxl/djxl | [✓] exiftool | [✓] magick | [✓] tifffile | [✓] pillow | [✓] imagecodecs | [✓] rich                    │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────── Main Menu ────────────────────────────────────────────────────╮
│  1  New workflow                                                                                                   │
│  2  Repeat last workflow (unknown)                                                                                 │
│  3  Check dependencies again                                                                                       │
│  4  Edit default settings                                                                                          │
│  5  Reset all settings                                                                                             │
│  6  Move settings file                                                                                             │
│  7  Presets (2 saved)                                                                                              │
│  0  Exit                                                                                                           │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

The wizard guides you through: Source format → Destination → Directory → Output mode → Parameters → Confirm.

Example session:

[1] New workflow
  Step 1: Source Format   → TIFF
  Step 2: Destination     → JXL d=0.1
  Step 3: Directory       → F:\Photos\2024
  Step 4: Mode            → 7 (Marker _EXPORT, subfolder)
  Step 5: Confirmation    → OK
  Step 6: Parameters      → Workers: 8, Distance: 0.1, Effort: 7
  Step 7: Summary         → Review and type YES to confirm
  → Executes the underlying script with all options

Tip: The value shown in blue (or between [brackets]/(parentheses)) is the default. Just press Enter to accept!

Example:

Workers [4]:          ← Press Enter to use 4
Distance [0.1]:       ← Press Enter to use 0.1
Execute? [y/n] (y):   ← Press Enter to accept 'y' (yes)

Auto Mode (New in v1.3)

Beta: Auto Mode is new and being actively tested. It works well for common folder structures, but if you encounter unexpected recommendations, use manual mode selection (0-8) which is fully stable and tested.

Instead of memorizing modes 0-8, press [A] in Step 4. The wizard scans your folder and:

  1. Analyzes structure (flat, recursive, export folders)
  2. Recommends best mode with confidence level
  3. Previews folder mappings (source → destination)

Auto Mode detection:

  • _EXPORT, Export_* → Mode 6/7 (Capture One/Lightroom workflows)
  • Deep subfolders → Mode 3 (recursive)
  • Multiple folders → Mode 2 (flat output)
  • Single folder → Mode 0 (in-place)

After analysis, choose:

  • [Y] Accept and run
  • [P] Generate manifest CSV → edit in Excel → [M] Run from manifest
  • [V] View manifest (if exists)
  • [N] Choose mode manually

Manifest System

Generate a CSV to edit before running:

Source,Destination,Mode,Direction
F:\2025\Tokyo\_Export\TIFF,F:\2025\Tokyo\_Export\TIFF,6,tiff2jxl
F:\2025\Kyoto\_EXPORT\16bit,F:\2025\Kyoto\_EXPORT\16B_JXL,7,tiff2jxl
# F:\2025\Osaka\RAW,F:\2025\Osaka\JXL,0,tiff2jxl
  • Edit paths, delete rows, reorder
  • Comment with # to skip
  • Rerun same manifest anytime
  • The Direction column binds the manifest to the workflow that generated it — running it from a different direction (e.g. a tiff2jxl manifest in a jxl2tiff session) is refused with a clear error instead of running the wrong script. Manifests without the column (older format) still run, with a warning.
  • Destination column: only modes 0 and 2 honor it. Modes 1/3/4/5/6/7/8 compute their own output locations from each script's settings (16B_JXL, converted_jxl, ...) — the wrapper prints a warning when a manifest entry's Destination is ignored.
  • Manifest compatibility: manifests are guaranteed to work with the version that generated them. Backward compatibility with older 2-column manifests is not guaranteed; regenerate the manifest if upgrading from a previous version.

Individual Scripts

Typical workflow (script commands)

Capture One
    ↓ Export 16-bit TIFF (sRGB, AdobeRGB, ProPhoto RGB)
jxl_tiff_encoder.py      TIFF → JXL  (archive, stays 16-bit, lossless or lossy)
    ↓
    JXLs on disk — ~8–47MB each for lossy, ~173MB for lossless (45MP example)
    ↓
jxl_tiff_decoder.py      JXL → TIFF  (when master TIFF is needed again)
    ↓ OR
jxl_jpeg_transcoder.py   JXL → JPEG/PNG  (when needed for print or delivery)
                                   ICC profile conversion applied here

TIFF → JXL

# Single file
py jxl_tiff_encoder.py "photo.tif"

# Folder (Capture One _EXPORT workflow)
py jxl_tiff_encoder.py "F:\Photos\2024" --mode 7

# With settings
py jxl_tiff_encoder.py "photo.tif" --mode 0 --workers 8

# Multi-page TIFF: split each real page into separate JXLs
py jxl_tiff_encoder.py "F:\Photos\2024" --mode 2 --multipage-mode split --thumbnail-mode exclude

# Multi-page TIFF: also export embedded thumbnails
py jxl_tiff_encoder.py "F:\Photos\2024" --mode 2 --multipage-mode split --thumbnail-mode include

JXL → TIFF

# Single file — auto mode (Roundtrip if has ICC, Basic if not)
py jxl_tiff_decoder.py "photo.jxl"

# Force Matrix mode for color space conversion
py jxl_tiff_decoder.py "photo.jxl" --matrix --target-icc "C:\icc\sRGB.icc"

# Folder
py jxl_tiff_decoder.py "F:\Photos\2024" --mode 7

# 8-bit output for web
py jxl_tiff_decoder.py "photo.jxl" --depth 8

# Reconstruct multi-page TIFF and drop thumbnail pages
py jxl_tiff_decoder.py "F:\Photos\2024" --mode 2 --thumbnail-handling ignore

JPEG ↔ JXL / JXL → PNG

# JPEG → JXL (lossless transcoding)
py jxl_jpeg_transcoder.py "F:\Photos\2024"

# JXL → JPEG (auto: lossless recovery if jbrd present, else lossy)
py jxl_jpeg_transcoder.py "F:\Photos\2024" --mode 8

# JXL → PNG 16-bit (archival)
py jxl_jpeg_transcoder.py "F:\Photos\2024" --format png

# JXL → sRGB JPEG (ICC conversion via ImageMagick)
py jxl_jpeg_transcoder.py "F:\Photos\2024" --to-srgb --quality 95

After conversion

Depending on your needs, three common approaches:

  1. Keep both TIFF and JXL — exclude the TIFF export folders from backups to save space. Tools like FreeFileSync support folder filters that make this easy.
  2. Delete TIFFs, keep only JXL — a separate script for this can be found here: delete-tiff-exports
  3. Use the configurable option to delete TIFFs after conversion available in this script.

Documentation

Document Contents
docs/README_jxl_tools.md Full documentation for the interactive wrapper
docs/README_jxl_tiff_encoder.md Full documentation for TIFF → JXL encoding
docs/README_jxl_tiff_decoder.md Full documentation for JXL → TIFF decoding
docs/README_jxl_jpeg_transcoder.md Full documentation for JPEG ↔ JXL / JXL → PNG
docs/jxl_color_internals.md Deep dive: XYB, ICC blobs vs primaries, troubleshooting
docs/version_history.md "What's New" notes for releases before v1.8
deprecated/README_jxl_to_jpg_png.md Deprecated — JXL → JPG/PNG (superseded by jxl_jpeg_transcoder.py)

Requirements & Installation

1. Python 3.9+ and Packages

# Install required packages
pip install tifffile numpy pillow rich imagecodecs

Important: Install packages in the same Python version you'll use to run the scripts.

2. External Tools (Download Executables, NOT Source Code)

Tool Download URL What to Download Extract to
cjxl / djxl https://github.com/libjxl/libjxl/releases jxl-x64-windows-static.zip (NOT jxl-x64-windows.zip) C:\tools\libjxl\ or your choice
exiftool https://exiftool.org exiftool-XX.XX_64.zip (Windows .zip, NOT .tar.gz) C:\tools\exiftool\ or your choice
ImageMagick https://imagemagick.org Installer .exe (Q16-HDRI x64) Default location

Tested dependency versions

Versions used before and after the dependency update on 2026-07-12 (last tested commit: f390463):

Component Tested until commit f390463 (2026-07-12) Current (recommended)
libjxl (cjxl/djxl) v0.11.2 v0.12.0
numpy 2.4.3 2.5.1
tifffile 2026.3.3 2026.6.1
Pillow 12.1.1 12.3.0
imagecodecs 2026.3.6 2026.6.26
rich 14.3.3 15.0.0
exiftool 13.52 13.59
ImageMagick 7.1.2-17 7.1.2-27

Older versions may still work, but the current versions are what we test against.

libjxl v0.12: the scripts auto-detect the cjxl/djxl version and adapt — lossless JPEG recovery uses djxl --reconstruct_jpeg (authoritative lossless guarantee), and pixel encodes can opt into --buffering 0 (best compression, ~6× slower on large lossless TIFFs; see the v1.8.0 benchmark). On libjxl < 0.12 everything behaves as before; no v0.12-only flag is ever passed.

Common Download Mistakes

Wrong Download Why It Fails Correct Download
jxl-x64-windows.zip Only DLLs, no executables jxl-x64-windows-static.zip
exiftool-XX.XX.tar.gz Perl source code, needs Perl installed exiftool-XX.XX_64.zip (Windows executable)

exiftool Setup

Note (v1.3+): The scripts now automatically detect both exiftool.exe and exiftool(-k).exe. Renaming is no longer required, but still works if you prefer.

The Windows download comes as exiftool(-k).exe. For v1.2 and earlier, you need to rename it:

# Option A: Rename the file
Rename-Item "C:\tools\exiftool\exiftool(-k).exe" "exiftool.exe"

# Option B: Duplicate and rename (keeps the original)
Copy-Item "C:\tools\exiftool\exiftool(-k).exe" "C:\tools\exiftool\exiftool.exe"

The (-k) suffix means "keep console open" — the original behavior. The scripts now handle both names automatically.

3. Add to PATH (PowerShell)

Replace the example paths below with YOUR actual installation paths:

# EDIT THESE PATHS to match where YOU extracted the tools:
$myPaths = @(
    "C:\tools\libjxl\bin",                           # where cjxl.exe and djxl.exe are
    "C:\tools\exiftool",                              # where exiftool.exe is (RENAMED!)
    "C:\Program Files\ImageMagick-7.1.1-Q16-HDRI"     # where magick.exe is
)

# Add to user PATH
$p = [Environment]::GetEnvironmentVariable("PATH", "User")
[Environment]::SetEnvironmentVariable("PATH", ($myPaths -join ";") + ";$p", "User")

# RESTART your PowerShell/terminal after this!

4. Verify Installation

Important: All tools must be in your PATH for both the wrapper and individual scripts to find them. The wrapper and scripts only search the system PATH — they do not look in other directories.

Restart PowerShell, then run:

# Each should return a version number
cjxl --version          # Should show: cjxl v0.XX.X
exiftool -ver           # Should show: 12.XX or 13.XX
magick -version         # Should show: ImageMagick version
python -c "import tifffile, PIL, rich; print('All Python packages OK')"

# Test full environment
cd "C:\Users\YourName\Documents\GitHub\jxl-photo"  # adjust path
py jxl_photo.py

You should see: [✓] cjxl/djxl | [✓] exiftool | [✓] magick | [✓] tifffile | [✓] pillow | [✓] imagecodecs | [✓] rich

Troubleshooting

Problem Cause Solution
cjxl not recognized Downloaded jxl-x64-windows.zip (runtime DLLs only) Download jxl-x64-windows-static.zip
exiftool not recognized File still named exiftool(-k).exe (v1.2 and earlier) For v1.3+, automatic detection works. For earlier versions, rename to exiftool.exe (see step 2)
exiftool returns nothing Downloaded .tar.gz (Perl source) Download .zip with _64 suffix
ModuleNotFoundError Packages in different Python version Run python -m pip install tifffile numpy pillow rich
PATH not working Terminal not restarted Close and reopen PowerShell completely

Alternative: Simpler Setup (TIFF lossless compression)

If the setup above feels overwhelming, there's an easier alternative that requires less installation:

A standalone PowerShell script that compresses TIFFs using ZIP/Deflate compression.

What you need:

  • PowerShell 7 (or Windows PowerShell 5.1)
  • ImageMagick
  • ExifTool

What's NOT needed:

  • Python or Python packages
  • libjxl (cjxl/djxl)

Compression comparison:

Format 16-bit Size 8-bit Size
Uncompressed TIFF ~260 MB ~130 MB
ZIP/Deflate (PowerShell) ~220 MB (~15% smaller) ~65 MB (~50% smaller)
JXL lossless (this toolkit) ~173 MB (~35% smaller) ~43 MB (~67% smaller)
JXL lossy d=0.1 (this toolkit) ~34 MB (~87% smaller) ~8 MB (~94% smaller)

Trade-off: Easier to install, but less compression than JXL. Still better than nothing!

When you're comfortable with ImageMagick and ExifTool, come back here for JXL with much better compression.


ICC Preservation: How It Works

Without This Toolkit (default cjxl behavior)

TIFF (ProPhoto ICC with Kodak TRC curves)
    ↓ cjxl lossy (default settings)
JXL (native primaries + minimal ICC - TRC detail optimized away)
    ↓ djxl
TIFF (generic ICC generated from primaries - sufficient for display only)

Problem: Generic ICC works for viewing, but lacks:

  • Precise tone reproduction curves (TRC)
  • Copyright and manufacturer metadata
  • Device-specific calibration

With This Toolkit

TIFF (ProPhoto ICC with Kodak TRC curves)
    ↓ jxl_tiff_encoder.py (EMBED_ICC_IN_JXL = True)
JXL (native primaries + XMP with base64 ICC)
    ↓ jxl_tiff_decoder.py (Roundtrip Mode)
TIFF (original ProPhoto ICC restored!)

Result: Exact original ICC with all metadata intact.

Technical Details

The ICC is base64-encoded and stored in XMP:

<xmp:CreatorTool>ICC:AAADrEtDTVMCEAAAbW50clJHQiBYWVog...</xmp:CreatorTool>
<dc:description>cjxl d=0.1 e=7</dc:description>
  • xmp:CreatorTool: Base64 ICC data with "ICC:" prefix (for round-trip preservation)
  • dc:Description: Encoding params cjxl d=X e=Y (visible in Windows Properties)

→ See docs/jxl_color_internals.md for full technical details.


Recommended Settings

Archival (Master Files)

# jxl_tiff_encoder.py
CJXL_DISTANCE = 0.05      # Near-lossless, ~47MB for 45MP
#OR#
CJXL_DISTANCE = 0.1       # Also Near-lossless, ~34MB for 45MP

CJXL_EFFORT = 7           # Good compression speed tradeoff
EMBED_ICC_IN_JXL = True   # Always preserve ICC!

Web / Delivery

# jxl_tiff_encoder.py
CJXL_DISTANCE = 1.0       # Visually lossless, ~8MB
CJXL_EFFORT = 7

# jxl_tiff_decoder.py
DJXL_OUTPUT_DEPTH = 8     # Smaller files
TIFF_COMPRESSION = "zip"
ADD_JPEG_PREVIEW = True   # Fast Explorer thumbnails

Configuration File Location

The wrapper (jxl_photo.py) saves settings in .jxl_tools_config.json:

  1. First priority: Script directory (where jxl_photo.py is located)
  2. Fallback: User home directory (%USERPROFILE% on Windows, ~ on Linux/Mac)

This allows per-project configurations — place a config file in the script folder for project-specific settings, or use the user home for global defaults.

To move settings between locations: use option 6 in the main menu.


Verifying ICC Preservation

# After TIFF → JXL → TIFF round-trip:

# Check original ICC
exiftool -ProfileDescription -ProfileCopyright original.tif

# Check round-trip ICC
exiftool -ProfileDescription -ProfileCopyright roundtrip.tif

# Should match exactly!

# Check ICC is embedded in JXL (ICC lives in XMP CreatorTool)
exiftool -XMP-xmp:CreatorTool photo.jxl | findstr "ICC:"

# Check EXIF is visible in IrfanView
exiftool -Make -Model roundtrip.tif

Known Limitations & Behaviors

Multi-page TIFFs: every page is converted by default

--multipage-mode defaults to split — one JXL per page (photo.jxl, photo_page1.jxl, ...), rejoined into a multi-page TIFF on decode. A TIFF with a single real page produces exactly photo.jxl, so for ordinary photos this is indistinguishable from the old default.

This default changed. It used to be ignore (page 0 only, everything else discarded), which caught people out: plenty of TIFFs are multi-page without looking like it — Capture One and many scanners append an embedded preview, and film scanners add an IR/mask page. Worse, mode 8 then deleted the source after encoding page 0, destroying the other pages permanently.

Embedded thumbnail/preview pages are still dropped by default (--thumbnail-mode exclude); they are reduced-resolution copies of a page that is already in the output. Add --thumbnail-mode include if you want the decoded TIFF to reproduce the original page structure exactly.

py jxl_tiff_encoder.py "F:\Photos"                             # split, thumbnails dropped
py jxl_tiff_encoder.py "F:\Photos" --thumbnail-mode include    # keep the previews too
py jxl_tiff_encoder.py "F:\Photos" --multipage-mode ignore     # old behavior: page 0 only

In the wizard the setting lives under Advanced Options (Step 6A — answer y when asked "Configure advanced options?"), and the Step 7 summary spells out the policy before you type YES. If you do choose a page-dropping policy, the encoder reports the totals in the run summary — and mode 8 refuses to delete any source whose pages were dropped, so you cannot lose them by accident.

Lossy is the default: --distance 0.1

The default is near-lossless, not lossless. At d=0.1 a 45 MP file drops to roughly a tenth of the TIFF size, and a difference blend in Photoshop will show small deviations — that is the compression working as configured, not a bug. For a bit-exact archive use --distance 0 (still ~40% smaller than an uncompressed TIFF). Note that --mode (0–8) only decides where output files go; quality is --distance alone.

Film scanners: IR channel / Digital ICE

If your scanner software (e.g. SilverFast, VueScan) uses the IR page as a hidden channel for Digital ICE / dust & scratch removal, converting the TIFF to JXL and back may break that feature. Those programs often rely on vendor-specific tags and exact page ordering beyond the standard TIFF SubfileType. This tool preserves the page as a standard grayscale PAGE, but the original scanner software may no longer recognize it as an IR mask. Test with one file before batch-processing important film scans.

Scanner ICC profiles and lossy encoding

Scanner ICC profiles (e.g. SilverFast SFprofT) can cause cjxl to produce very dark images in lossy mode. The encoder works around this by not embedding the ICC in the intermediate PNG and restoring it into the reconstructed TIFF (see --icc-png-strategy). The JXL file may therefore display with shifted colors in some viewers, but the TIFF round-trip is accurate. For scanner workflows, treat JXL as the backup container and the reconstructed TIFF as the final image.

Viewer quirks (not data loss)

Viewer Behavior Why
Windows Explorer Thumbnails ignore the embedded EXIF thumbnail and are not color-managed — ProPhoto/Adobe RGB images look washed out Limitation of Microsoft's JXL WIC codec
IrfanView EXIF visible for TIFF → JXL, hidden for JPEG → JXL (lossless or lossy) JPEG → JXL uses Brotli (brob box), which IrfanView cannot read
IrfanView / XnView MP Wide-gamut JXL may look slightly muted on a calibrated monitor Viewer rendering limitation — the file keeps the full gamut; decode back to TIFF to confirm
XnView MP Shows Color Profile: sRGB for lossy JXL regardless of the real space Lossy JXL stores compact numeric primaries, not an ICC blob; XnView falls back to an "sRGB" label

For reliable EXIF and color, use XnView MP or digiKam. If an image looks heavily desaturated, that is a real bug — please report it.

Default re-run behavior differs per script

The TIFF encoder/decoder default to smart sync (reconvert when the source is newer than the existing output — there is no plain "skip existing" CLI mode), while the JPEG transcoder skips existing outputs by default. Use --overwrite (always) or --sync (source newer) to control it explicitly. See each script's README for details.

Matrix decode mode is 8-bit internally

The decoder's Matrix mode (--matrix, for color-space conversion via LittleCMS) quantizes pixels to 8-bit for the transform and scales the result back to 16-bit. Effective precision is 8 bits in that mode only — use Roundtrip mode (the default) for full 16-bit fidelity.

eciRGB v2 and Special ICC Profiles

The cjxl/dxjl converters were optimized for:

  • sRGB (gamma ~2.2)
  • Rec.2020 (standard gamma)
  • Linear spaces

Profiles with special transfer curves like eciRGB v2 (L* curve) may have slight color shifts during conversion because cjxl/djxl assumes standard gamma when encoding to XYB.

Recommendation: For critical work with eciRGB v2 or similar profiles, either:

  • Keep originals in TIFF format, or
  • Convert to Rec.2020 before JXL encoding

See docs/jxl_color_internals.md for technical details.


Changelog


Disclaimer

These tools were made for my personal workflow. Use at your own risk — I am not responsible for any issues you may encounter.

However, If you find any bugs, feel free to report to me - I will gladly try my best to improve this project.

Always test with a small batch before processing important archives.


More about this project

I am sharing these scripts because getting all of this to work correctly was unexpectedly difficult. The challenges were:

  • Preserving 16-bit depth through the conversion pipeline
  • Embedding EXIF so it is visible in IrfanView and other applications
  • Correctly handling ICC profiles from Capture One exports (sRGB, AdobeRGB, ProPhoto RGB)
  • Fixing XMP overwrite bug that destroyed original metadata
  • Fixing EXIF binary extraction that produced corrupted data
  • Sync mode — reconverting only re-exported photos in existing folders
  • Performance — RAM usage, parallelism, and staging to minimize I/O

Getting there required finding and fixing several bugs that appears because of the specific combination of softwares I use (Capture One, cjxl, exiftool, IrfanView). Those bugs and their fixes are documented in docs/bugs_fixes_explained.md.


License

MIT License — feel free to use, modify, and distribute.


Acknowledgments

  • libjxl team for JPEG XL implementation
  • ExifTool by Phil Harvey for metadata handling
  • tifffile by Christoph Gohlke for TIFF I/O
  • Kimi (Moonshot AI) and Claude (Anthropic) for code assistance and technical discussion

About

JPEG XL toolkit for photographers. Full ICC profile and 16-bit depth preservation — lossless or lossy. TIFF↔JXL, JPEG↔JXL, JXL→PNG with EXIF/XMP. Multiple folder modes for Capture One and Lightroom.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages