diff --git a/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsImageFilterApiImpl.java b/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsImageFilterApiImpl.java
index 538314e129de..1fef2d8e3cad 100644
--- a/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsImageFilterApiImpl.java
+++ b/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsImageFilterApiImpl.java
@@ -69,6 +69,8 @@ public class VipsImageFilterApiImpl implements ImageFilterAPI {
// libvips-only capabilities with no legacy equivalent
.put("smartcrop", VipsSmartCropImageFilter.class)
.put("avif", VipsAvifImageFilter.class)
+ .put("jxl", VipsJpegXlImageFilter.class)
+ .put("jpegxl", VipsJpegXlImageFilter.class)
.build();
public VipsImageFilterApiImpl() {
diff --git a/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsJpegXlImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsJpegXlImageFilter.java
new file mode 100644
index 000000000000..85c64d94eae3
--- /dev/null
+++ b/dotCMS/src/main/java/com/dotmarketing/image/vips/VipsJpegXlImageFilter.java
@@ -0,0 +1,47 @@
+package com.dotmarketing.image.vips;
+
+import app.photofox.vipsffm.VImage;
+import app.photofox.vipsffm.VipsOption;
+import java.io.File;
+import java.util.Map;
+
+/**
+ * JPEG XL encoder — a modern format the legacy engine cannot produce. Encodes via libvips' libjxl
+ * delegate, typically smaller than JPEG at equal quality with support for lossless re-compression of
+ * existing JPEGs.
+ *
+ *
URL contract: {@code filter=jxl&jxl_q=75}. {@code q} is 0-100 quality; {@code lossless=true}
+ * switches to mathematically lossless encoding; {@code effort} (1-9) trades encode speed for size.
+ * Requires the host libvips to be built with libjxl; if absent the request fails (no legacy
+ * fallback — JPEG XL is libvips-only).
+ */
+public class VipsJpegXlImageFilter extends VipsImageFilter {
+
+ @Override
+ public String[] getAcceptedParameters() {
+ return new String[] {
+ "q (int) 0-100 quality",
+ "lossless (present) lossless encode",
+ "effort (int) 1-9 encoder effort/speed tradeoff"
+ };
+ }
+
+ @Override
+ protected void transform(final File in, final File out, final Map parameters) {
+ final int quality = intParam(parameters, "q", 75);
+ final boolean lossless = parameters.get(getPrefix() + "lossless") != null;
+ final int effort = intParam(parameters, "effort", 7);
+ VipsManager.run(arena -> {
+ final VImage src = VipsManager.load(arena, in);
+ src.writeToFile(out.getAbsolutePath(),
+ VipsOption.Int("Q", quality),
+ VipsOption.Boolean("lossless", lossless),
+ VipsOption.Int("effort", effort));
+ });
+ }
+
+ @Override
+ public File getResultsFile(final File file, final Map parameters) {
+ return getResultsFile(file, parameters, "jxl");
+ }
+}
diff --git a/dotCMS/src/test/java/com/dotmarketing/image/vips/VipsParityTest.java b/dotCMS/src/test/java/com/dotmarketing/image/vips/VipsParityTest.java
index b526527e0c9d..95b8c2f9c479 100644
--- a/dotCMS/src/test/java/com/dotmarketing/image/vips/VipsParityTest.java
+++ b/dotCMS/src/test/java/com/dotmarketing/image/vips/VipsParityTest.java
@@ -337,6 +337,37 @@ public void avif_encoder_produces_valid_avif() throws Exception {
assertTrue("expected avif brand, got '" + brand + "'", brand.startsWith("avi"));
}
+ /** JPEG XL encode requires the host libvips to be built with the libjxl delegate. */
+ private boolean jxlEncodeSupported() {
+ try {
+ final File probeIn = image("test.png");
+ final File probeOut = tempOut("jxl");
+ new VipsJpegXlImageFilter().transform(probeIn, probeOut, params("jxl_q", "75"));
+ return probeOut.exists() && probeOut.length() > 50;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Test
+ public void jxl_encoder_produces_valid_jxl() throws Exception {
+ Assume.assumeTrue("host libvips lacks a JPEG XL encoder (libjxl)", jxlEncodeSupported());
+ final File in = image("test.png");
+ final File out = tempOut("jxl");
+ new VipsJpegXlImageFilter().transform(in, out, params("jxl_q", "75"));
+ assertTrue("jxl written", out.exists() && out.length() > 50);
+ // JPEG XL signatures: the raw codestream starts with FF 0A; the ISO-BMFF container starts
+ // with a 'JXL ' box (00 00 00 0C 4A 58 4C 20).
+ final byte[] head = new byte[12];
+ try (java.io.InputStream is = new java.io.FileInputStream(out)) {
+ assertEquals(12, is.read(head));
+ }
+ final boolean rawCodestream = (head[0] & 0xFF) == 0xFF && (head[1] & 0xFF) == 0x0A;
+ final boolean container = (head[4] & 0xFF) == 0x4A && (head[5] & 0xFF) == 0x58
+ && (head[6] & 0xFF) == 0x4C && (head[7] & 0xFF) == 0x20;
+ assertTrue("expected a JPEG XL signature", rawCodestream || container);
+ }
+
@Test
public void smartcrop_produces_exact_box_from_salient_region() throws Exception {
final File in = image("test.jpg");