From: Svjatoslav Agejenko Date: Sun, 22 Feb 2026 18:17:12 +0000 (+0200) Subject: Add support for decoding JPEG XL (JXL) images X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=931ee6e853cde0b9124d73cb0d5f5679edfbecb8;p=meviz.git Add support for decoding JPEG XL (JXL) images --- diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/Constants.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/Constants.java index d302472..25f0826 100755 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/Constants.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/Constants.java @@ -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"}; diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java index 456649d..fabbce9 100755 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/Utils.java @@ -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) { diff --git a/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/DirectoryMetadata.java b/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/DirectoryMetadata.java index 0725382..4bbf8f4 100755 --- a/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/DirectoryMetadata.java +++ b/src/main/java/eu/svjatoslav/meviz/htmlindexer/metadata/DirectoryMetadata.java @@ -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);