Add support for decoding JPEG XL (JXL) images
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 22 Feb 2026 18:17:12 +0000 (20:17 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 22 Feb 2026 18:17:12 +0000 (20:17 +0200)
src/main/java/eu/svjatoslav/meviz/htmlindexer/Constants.java
src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java
src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/DirectoryMetadata.java

index d302472..25f0826 100755 (executable)
@@ -31,7 +31,7 @@ public class Constants {
             "E-mail: svjatoslav@svjatoslav.eu, homepage: http://svjatoslav.eu";
 
     public static final String[] SUPPORTED_IMAGE_EXTENSIONS = {
-            "jpg", "jpeg", "png", "gif", "webp"};
+            "jpg", "jpeg", "png", "gif", "webp", "jxl"};
 
     public static final String[] SUPPORTED_VIDEO_EXTENSIONS = {
             "avi", "mp4", "mpeg", "mpg", "mkv", "flv", "ogv"};
index 456649d..fabbce9 100755 (executable)
@@ -6,12 +6,14 @@
 
 package eu.svjatoslav.meviz.htmlindexer;
 
+import eu.svjatoslav.commons.file.FilePathParser;
 import eu.svjatoslav.meviz.htmlindexer.layouts.Layout;
 import eu.svjatoslav.meviz.htmlindexer.layouts.MixedLayout;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
+import javax.imageio.ImageIO;
 import javax.imageio.ImageReader;
 import javax.imageio.metadata.IIOMetadata;
 import javax.imageio.metadata.IIOMetadataNode;
@@ -44,7 +46,12 @@ public class Utils {
         if (file.equals(lastLoadedFile))
             return lastLoadedBufferedImage;
 
-        lastLoadedBufferedImage = createBufferedImage(readMBF(file));
+        String extension = FilePathParser.getFileExtension(file.getName()).toLowerCase();
+        if ("jxl".equals(extension)) {
+            lastLoadedBufferedImage = loadJxlImage(file);
+        } else {
+            lastLoadedBufferedImage = createBufferedImage(readMBF(file));
+        }
         lastLoadedFile = file;
 
         if (lastLoadedBufferedImage == null) {
@@ -57,6 +64,80 @@ public class Utils {
         return lastLoadedBufferedImage;
     }
 
+    /**
+     * Load a JPEG XL (.jxl) image by converting it to PNG using the djxl decoder
+     * from libjxl-tools, since Java's ImageIO and OpenIMAJ do not natively
+     * support JPEG XL. Requires libjxl-tools (djxl) to be installed.
+     */
+    private static BufferedImage loadJxlImage(final File jxlFile)
+            throws ImageFormatError, IOException {
+        File tempPng = null;
+        try {
+            tempPng = File.createTempFile("meviz_jxl_", ".png");
+            tempPng.deleteOnExit();
+
+            System.out.println("Decoding JXL: \"" + jxlFile.getName() + "\"");
+
+            final Runtime runtime = Runtime.getRuntime();
+            final Process process = runtime.exec(new String[]{
+                    "djxl", jxlFile.getAbsolutePath(), tempPng.getAbsolutePath()});
+
+            // Read stderr in a separate thread to prevent blocking
+            final StringBuilder stderrBuilder = new StringBuilder();
+            final Thread stderrThread = new Thread(() -> {
+                try (java.io.InputStream is = process.getErrorStream();
+                     java.io.InputStreamReader isr = new java.io.InputStreamReader(is);
+                     BufferedReader br = new BufferedReader(isr)) {
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        stderrBuilder.append(line).append("\n");
+                    }
+                } catch (IOException e) {
+                    stderrBuilder.append("Error reading stderr: ").append(e.getMessage());
+                }
+            });
+            stderrThread.start();
+
+            final int exitCode;
+            try {
+                exitCode = process.waitFor();
+                stderrThread.join();
+            } catch (InterruptedException e) {
+                throw new IOException("JXL decoding interrupted for: " + jxlFile, e);
+            }
+
+            final String stderr = stderrBuilder.toString().trim();
+            if (!stderr.isEmpty()) {
+                System.out.println("  djxl stderr: " + stderr);
+            }
+
+            if (exitCode != 0) {
+                throw new ImageFormatError(
+                        "djxl failed to decode JXL file: " + jxlFile
+                                + " (exit code " + exitCode + "). "
+                                + "stderr: " + stderr + ". "
+                                + "Ensure libjxl-tools (djxl) is installed.");
+            }
+
+            System.out.println("  JXL decoded, temp PNG size: " + tempPng.length() + " bytes");
+
+            final BufferedImage image = ImageIO.read(tempPng);
+            if (image == null) {
+                throw new ImageFormatError(
+                        "Failed to read converted PNG from JXL file: " + jxlFile);
+            }
+
+            System.out.println("  JXL image loaded: "
+                    + image.getWidth() + "x" + image.getHeight());
+
+            return image;
+        } finally {
+            if (tempPng != null && tempPng.exists()) {
+                tempPng.delete();
+            }
+        }
+    }
+
     public static File getLayoutIndexFile(final Layout layout,
                                           final File directoryToIndex) {
 
index 0725382..4bbf8f4 100755 (executable)
@@ -65,7 +65,9 @@ public class DirectoryMetadata implements Serializable {
             try {
                 return new Picture(parentDirectory, fileName);
             } catch (final Exception exception) {
-                System.out.println("Failed to decode image \"" + fileName +"\" indexing as normal file instead.");
+                System.out.println("Failed to decode image \"" + fileName
+                        + "\" indexing as normal file instead. Reason: " + exception.getMessage());
+                exception.printStackTrace();
 
                 // in case image decoding failed, handle image as general file
                 return new GeneralFile(parentDirectory, fileName);