Image Processing Efficiency with Directory Opus

Image Processing Efficiency with Directory Opus

How I use Dopus background scripting to automate JXL conversions and speed up my image editing workflow.

Maybe it’s the design thinking in me, or maybe it’s floating somewhere in my adhd neurodivergency and itch-scratching, but I just can’t help but build a better mousetrap when I see one and the lift is within my care zone tolerance.

I do a lot of computer work, and those who know me know that I can go down nerdy rabbit-holes of epic proportions. Particularly when there’s this convergence of grinding repetitive tasks and technology and process where I just can’t help myself but do something about it. So as someone who does a ton of computer work, there’s nothing as soul-sucking to me as repetitive file maintenance tasks. This is what led me to become a fan of Directory Opus. While it is heavier weight than the standard file explorer, there are just so many opportunities to make my life easier within it. I swear to you, they aren’t paying me and I’m not getting any affiliate kickbacks. Which is to say that it is a legitimately great solution if you’re doing this kind of work.

Recently, I went down a separate rabbit hole of discovering JPEG-XL format. As a former pro-am photographer and Lightroom enthusiast, I love this new(ish) format. It is kind of the mother of image formats: capable of being lossless or lossy, HDR ready, alpha channel capable, with modern compression. It’s what’s under the hood in iPhone static raw image formats. It’s pretty great.

So I wanted a workflow that would help me adopt the new format by automatically flipping files to JXL via a toggle-able monitoring process. But also I wanted a way to handle various contingencies: What if I need the old format? What if I need the old format temporarily but long term want to stay in JXL? How can I deal with both of those things without having to remember to turn the monitoring off every time I want to do an ad-hoc process?

By combining Google Gemini 3.8 Pro with Directory Opus 13, I was able to build a script and a set of buttons capable of handling all of this. Below are the details of the script and how to install it. Please let me know if you have feedback!

How the Pipeline Works

This whole thing runs as a background Script Add-in. If you aren’t familiar, script add-ins in Directory Opus are essentially plain text files (using a .js extension for JScript) that live in your Opus Script AddIns folder. Because they are event-driven rather than just sitting in a button waiting to be clicked, they can automatically trigger in response to specific actions—like navigating between folders.

When the auto-conversion toggle is turned on, the script monitors your navigation using the OnAfterFolderChange event. As soon as you open a folder, it natively queues up any local JPEGs and PNGs and silently funnels them through the JPEG-XL command-line interface.

The conversions are completely strict: JPEGs use a bit-exact container transcoding, and PNGs use pixel-perfect lossless compression. The script only replaces your files 1-for-1 if the new .jxl container is genuinely smaller than the original. And because losing timeline metadata is a huge pain, the script explicitly preserves the original NTFS modification and creation timestamps.

To prevent infinite loops when you intentionally restore an image back to a legacy format, it writes Directory Opus metadata tags (Orig_JPG, Orig_PNG, and JXL_Ignore) natively to the files. It even looks for an active 32-bit alpha layer on JXL files. If you try to blindly convert a transparent image to a JPEG, the script catches it and throws a detached dialog (an interactive prompt that evaluates in real time without locking up the script) to give you safe routing options so you don’t accidentally nuke your transparency.

Step 1: Install the CLI Requirements

Because this script leverages the official libjxl binaries to do the heavy lifting, you’ll need to drop those onto your machine first.

  1. Download the latest Windows release of the command-line tools from the official libjxl repository.
  2. Extract the files and place them in this exact directory structure so the script can find them: C:\Program Files\jxl-x64-windows\x64-windows\bin\
  3. Verify that both cjxl.exe and djxl.exe are sitting in that bin folder.

Step 2: Install the Script Add-in

To install this script, type /dopusdata/Script AddIns straight into your Directory Opus location field and hit Enter. Save the code block at the very bottom of this post as a .js file (something like AutoJXLPipeline.js) and drop it into that folder. Directory Opus will recognize and initialize it immediately.

Step 3: Wire Up the Toolbar Buttons

The script automatically registers seven custom internal commands using initData.AddCommand(). You can assign these directly to your standard Opus toolbar buttons. Just right-click your toolbar, select Customize, create new buttons, and paste these exact names into the Command field:

  • JXLToggle: Toggles the background folder monitoring on or off globally.
  • JXLRestore: Ad-hoc decoding. Select a .jxl file and click this to safely revert it to its exact original .jpg or .png format. It applies an ignore tag so the background monitor doesn’t instantly snap it back.
  • JXLReconvert: Ad-hoc encoding. Select a .jpg or .png and click this to manually convert it to .jxl without turning the global background monitor on.
  • JXLConvertToJPG: Copies a JXL file to a standard JPEG (leaving the JXL file intact).
  • JXLConvertToPNG: Copies a JXL file to a standard PNG (leaving the JXL file intact).
  • JXLFlipToJPG: Converts a JXL file to a JPEG and permanently trashes the original JXL file.
  • JXLFlipToPNG: Converts a JXL file to a PNG and permanently trashes the original JXL file.

(Side note: If you ever want to bundle this script alongside your own custom button icons to share with someone else, you can package them into a zip archive with an .osp extension to create a deployable Script Package.)

The Script Code (Version 1.2)

JavaScript
// ========================================================================
// Auto JXL Pipeline for Directory Opus
// ========================================================================
// Created by: Berkeley Goodloe
// Version: 1.2
// License: Free for the community to use. Modifications should be reattributed.
//
// Recently I've been working more in JXL (JPEG-XL) format but I got tired of 
// doing manual file conversions. Using the below script, I have Dopus handling 
// that on the fly, with means to revert the conversions back and forth on an 
// adhoc basis, as well as a way to toggle the auto-converting on and off.
//
// DESCRIPTION:
// This script operates as a background monitor that automatically converts 
// existing local JPEGs and PNGs to the JXL format when opening a folder. 
// JPEGs use bit-exact container transcoding and PNGs use pixel-perfect 
// compression. Files are strictly replaced 1-for-1 only if the JXL version 
// is smaller. Original creation/modification timestamps are preserved natively.
// 
// The script accounts for seven custom commands to be used as toolbar buttons:
// - JXLToggle: Toggles the background folder monitoring on or off globally.
// - JXLRestore: Ad-hoc decoding to safely revert a .jxl to its original format.
// - JXLReconvert: Ad-hoc encoding to manually convert a file back to .jxl.
// - JXLConvertToJPG: Copies JXL to JPEG (preserves original JXL file).
// - JXLConvertToPNG: Copies JXL to PNG (preserves original JXL file).
// - JXLFlipToJPG: Converts JXL to JPEG and deletes the original JXL file.
// - JXLFlipToPNG: Converts JXL to PNG and deletes the original JXL file.
//
// CONFIGURATION (CLI INSTALLATION):
// To align with this script, you must install the official libjxl binaries:
// 1) Download the latest Windows release of the command-line tools.
// 2) Extract the files and place them in this exact directory structure:
//    C:\Program Files\jxl-x64-windows\x64-windows\bin\
// 3) Verify that both cjxl.exe and djxl.exe are present in that bin folder.
// ========================================================================

function OnInit(initData) {
    initData.name = "Auto JXL Pipeline";
    initData.desc = "Background conversion and multi-format manual tools.";
    initData.copyright = "Pipeline";
    initData.default_enable = true;

    var cmdToggle = initData.AddCommand();
    cmdToggle.name = "JXLToggle";
    cmdToggle.method = "OnJXLToggle";

    var cmdRestore = initData.AddCommand();
    cmdRestore.name = "JXLRestore";
    cmdRestore.method = "OnJXLRestore";

    var cmdReconvert = initData.AddCommand();
    cmdReconvert.name = "JXLReconvert";
    cmdReconvert.method = "OnJXLReconvert";

    var cmdConvJpg = initData.AddCommand();
    cmdConvJpg.name = "JXLConvertToJPG";
    cmdConvJpg.method = "OnJXLConvertToJPG";

    var cmdConvPng = initData.AddCommand();
    cmdConvPng.name = "JXLConvertToPNG";
    cmdConvPng.method = "OnJXLConvertToPNG";

    var cmdFlipJpg = initData.AddCommand();
    cmdFlipJpg.name = "JXLFlipToJPG";
    cmdFlipJpg.method = "OnJXLFlipToJPG";

    var cmdFlipPng = initData.AddCommand();
    cmdFlipPng.name = "JXLFlipToPNG";
    cmdFlipPng.method = "OnJXLFlipToPNG";
}

function OnJXLToggle(scriptCmdData) {
    var state = DOpus.Vars.Exists("JXL_AUTO_CONVERT") ? DOpus.Vars.Get("JXL_AUTO_CONVERT") : false;
    DOpus.Vars.Set("JXL_AUTO_CONVERT", !state);
}

function hasTag(item, tagToFind) {
    if (!item.metadata || !item.metadata.tags) return false;
    var tags = item.metadata.tags;
    for (var i = 0; i < tags.count; i++) {
        if (tags(i).toLowerCase() === tagToFind.toLowerCase()) return true;
    }
    return false;
}

function formatOpusDate(d) {
    function pad(n) { return n < 10 ? '0' + n : n; }
    return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + 
           pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}

function OnAfterFolderChange(afterFolderChangeData) {
    if (!afterFolderChangeData.result) return;
    if (!DOpus.Vars.Exists("JXL_AUTO_CONVERT") || !DOpus.Vars.Get("JXL_AUTO_CONVERT")) return;

    var tab = afterFolderChangeData.tab;
    if (tab.files.count === 0) return;

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");
    var fsUtil = DOpus.FSUtil;
    var cjxlPath = "C:\\Program Files\\jxl-x64-windows\\x64-windows\\bin\\cjxl.exe";

    for (var objEnum = new Enumerator(tab.files); !objEnum.atEnd(); objEnum.moveNext()) {
        var item = objEnum.item();
        var ext = item.ext.toLowerCase();
        
        if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") {
            if (hasTag(item, "JXL_Ignore")) continue;

            var originalPath = String(item.realpath); 
            var jxlPath = originalPath.substr(0, originalPath.length - ext.length) + ".jxl";

            if (fsUtil.Exists(jxlPath)) continue;

            var createDate = new Date(item.create);
            var modifyDate = new Date(item.modify);
            var originalSize = item.size;
            
            var tempJxlPath = jxlPath + ".tmp";
            var isJpg = (ext === ".jpg" || ext === ".jpeg");
            var params = isJpg ? "-j 1" : "-q 100";
            
            var cmdString = '"' + cjxlPath + '" ' + params + ' "' + originalPath + '" "' + tempJxlPath + '"';
            shell.Run(cmdString, 0, true);
            
            if (fsUtil.Exists(tempJxlPath)) {
                var newFile = fso.GetFile(tempJxlPath);
                
                if (newFile.Size < originalSize) {
                    fso.DeleteFile(originalPath);
                    fso.MoveFile(tempJxlPath, jxlPath);
                    
                    var tsCmd = DOpus.Create.Command();
                    tsCmd.ClearFiles();
                    tsCmd.AddFile(jxlPath);
                    
                    var modStr = formatOpusDate(modifyDate);
                    var creStr = formatOpusDate(createDate);
                    var origExtTag = isJpg ? "Orig_JPG" : "Orig_PNG";
                    
                    tsCmd.RunCommand('SetAttr MODIFIED="' + modStr + '" CREATED="' + creStr + '" META "tags:+' + origExtTag + '"');
                } else {
                    fso.DeleteFile(tempJxlPath);
                }
            }
        }
    }
}

function OnJXLRestore(scriptCmdData) {
    var tab = scriptCmdData.func.sourcetab;
    if (tab.selected_files.count === 0) return;

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");
    var fsUtil = DOpus.FSUtil;
    var djxlPath = "C:\\Program Files\\jxl-x64-windows\\x64-windows\\bin\\djxl.exe";

    for (var objEnum = new Enumerator(tab.selected_files); !objEnum.atEnd(); objEnum.moveNext()) {
        var item = objEnum.item();
        if (item.ext.toLowerCase() !== ".jxl") continue;

        var targetExt = ".jpg"; 
        if (hasTag(item, "Orig_PNG")) targetExt = ".png";
        
        var createDate = new Date(item.create);
        var modifyDate = new Date(item.modify);
        
        var originalPath = String(item.realpath);
        var restorePath = originalPath.substr(0, originalPath.length - 4) + targetExt;
        var tempRestorePath = originalPath.substr(0, originalPath.length - 4) + ".tmp" + targetExt;
        
        var cmdString = '"' + djxlPath + '" "' + originalPath + '" "' + tempRestorePath + '"';
        shell.Run(cmdString, 0, true);

        if (fsUtil.Exists(tempRestorePath)) {
            if (fsUtil.Exists(restorePath)) fso.DeleteFile(restorePath);
            fso.MoveFile(tempRestorePath, restorePath);
            fso.DeleteFile(originalPath);

            var tsCmd = DOpus.Create.Command();
            tsCmd.ClearFiles();
            tsCmd.AddFile(restorePath);
            
            var modStr = formatOpusDate(modifyDate);
            var creStr = formatOpusDate(createDate);
            tsCmd.RunCommand('SetAttr MODIFIED="' + modStr + '" CREATED="' + creStr + '" META "tags:+JXL_Ignore"');
        }
    }
}

function OnJXLReconvert(scriptCmdData) {
    var tab = scriptCmdData.func.sourcetab;
    if (tab.selected_files.count === 0) return;

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");
    var fsUtil = DOpus.FSUtil;
    var cjxlPath = "C:\\Program Files\\jxl-x64-windows\\x64-windows\\bin\\cjxl.exe";

    for (var objEnum = new Enumerator(tab.selected_files); !objEnum.atEnd(); objEnum.moveNext()) {
        var item = objEnum.item();
        var ext = item.ext.toLowerCase();
        
        if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png") continue;

        var originalPath = String(item.realpath); 
        var jxlPath = originalPath.substr(0, originalPath.length - ext.length) + ".jxl";

        if (fsUtil.Exists(jxlPath)) continue;

        var createDate = new Date(item.create);
        var modifyDate = new Date(item.modify);
        var originalSize = item.size;
        
        var tempJxlPath = jxlPath + ".tmp";
        var isJpg = (ext === ".jpg" || ext === ".jpeg");
        var params = isJpg ? "-j 1" : "-q 100";
        
        var cmdString = '"' + cjxlPath + '" ' + params + ' "' + originalPath + '" "' + tempJxlPath + '"';
        shell.Run(cmdString, 0, true);
        
        if (fsUtil.Exists(tempJxlPath)) {
            var newFile = fso.GetFile(tempJxlPath);
            
            if (newFile.Size < originalSize) {
                fso.DeleteFile(originalPath);
                fso.MoveFile(tempJxlPath, jxlPath);
                
                var tsCmd = DOpus.Create.Command();
                tsCmd.ClearFiles();
                tsCmd.AddFile(jxlPath);
                
                var modStr = formatOpusDate(modifyDate);
                var creStr = formatOpusDate(createDate);
                var origExtTag = isJpg ? "Orig_JPG" : "Orig_PNG";
                
                tsCmd.RunCommand('SetAttr MODIFIED="' + modStr + '" CREATED="' + creStr + '" META "tags:+' + origExtTag + '"');
            } else {
                fso.DeleteFile(tempJxlPath);
            }
        }
    }
}

// ------------------------------------------------------------------------
// ROUTING COMMANDS FOR EXPLICIT CONVERSIONS
// ------------------------------------------------------------------------
function OnJXLConvertToJPG(scriptCmdData) { ProcessJXLManual(scriptCmdData, ".jpg", false); }
function OnJXLConvertToPNG(scriptCmdData) { ProcessJXLManual(scriptCmdData, ".png", false); }
function OnJXLFlipToJPG(scriptCmdData) { ProcessJXLManual(scriptCmdData, ".jpg", true); }
function OnJXLFlipToPNG(scriptCmdData) { ProcessJXLManual(scriptCmdData, ".png", true); }

// ------------------------------------------------------------------------
// CENTRAL HELPER: Processes explicit JXL -> Image commands
// ------------------------------------------------------------------------
function ProcessJXLManual(scriptCmdData, targetFormat, deleteOriginal) {
    var tab = scriptCmdData.func.sourcetab;
    if (tab.selected_files.count === 0) return;

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");
    var fsUtil = DOpus.FSUtil;
    var djxlPath = "C:\\Program Files\\jxl-x64-windows\\x64-windows\\bin\\djxl.exe";

    var skipAlphaPrompt = false;
    var alphaAction = "";

    for (var objEnum = new Enumerator(tab.selected_files); !objEnum.atEnd(); objEnum.moveNext()) {
        var item = objEnum.item();
        if (item.ext.toLowerCase() !== ".jxl") continue;

        var targetExt = targetFormat;
        
        // Depth > 24 indicates 32-bit (RGBA) or 64-bit color depth (contains Alpha)
        if (targetExt === ".jpg") {
            var hasAlpha = (item.metadata && item.metadata.image && item.metadata.image.depth > 24);
            if (hasAlpha) {
                if (!skipAlphaPrompt) {
                    var dlg = scriptCmdData.func.Dlg;
                    dlg.title = "Alpha Channel Detected";
                    dlg.message = "The image '" + item.name + "' has an alpha/transparent layer. Converting to JPEG will strip this layer.\n\nAre you sure you want to proceed?";
                    
                    if (tab.selected_files.count === 1) {
                        dlg.buttons = "Yes|PNG instead|Cancel";
                        var result = dlg.Show();
                        if (result === 0) break; 
                        if (result === 1) targetExt = ".jpg";
                        if (result === 2) targetExt = ".png";
                    } else {
                        dlg.buttons = "Yes|Yes to all|PNG instead|PNG only for images with alpha channels|Cancel";
                        var result = dlg.Show();
                        if (result === 0) break;
                        if (result === 1) targetExt = ".jpg";
                        if (result === 2) { targetExt = ".jpg"; skipAlphaPrompt = true; alphaAction = "jpg"; }
                        if (result === 3) targetExt = ".png";
                        if (result === 4) { targetExt = ".png"; skipAlphaPrompt = true; alphaAction = "png"; }
                    }
                } else {
                    targetExt = (alphaAction === "png") ? ".png" : ".jpg";
                }
            }
        }

        var createDate = new Date(item.create);
        var modifyDate = new Date(item.modify);
        var originalPath = String(item.realpath);
        var restorePath = originalPath.substr(0, originalPath.length - 4) + targetExt;
        var tempRestorePath = originalPath.substr(0, originalPath.length - 4) + ".tmp" + targetExt;
        
        var cmdString = '"' + djxlPath + '" "' + originalPath + '" "' + tempRestorePath + '"';
        shell.Run(cmdString, 0, true);

        if (fsUtil.Exists(tempRestorePath)) {
            if (fsUtil.Exists(restorePath)) fso.DeleteFile(restorePath);
            fso.MoveFile(tempRestorePath, restorePath);
            
            // Evaluates the boolean flag dictated by the firing button
            if (deleteOriginal) {
                fso.DeleteFile(originalPath);
            }
            
            var tsCmd = DOpus.Create.Command();
            tsCmd.ClearFiles();
            tsCmd.AddFile(restorePath);
            
            var modStr = formatOpusDate(modifyDate);
            var creStr = formatOpusDate(createDate);
            tsCmd.RunCommand('SetAttr MODIFIED="' + modStr + '" CREATED="' + creStr + '" META "tags:+JXL_Ignore"');
        }
    }
}

Berkeley Goodloe is a seasoned business professional with over 25 years of experience in banking, payments, and start-ups. He received an MBA, with a concentration in supply chain management, from Virginia Commonwealth University, where he focused on liquidity, process improvement, and productivity.


file under:

Welcome

headshot portrait of Berkeley Goodloe

This is the personal homepage of Berkeley Goodloe, where you’ll find writings about topics of interest, problems I’m solving, and solutions I’m championing. This includes productivity, life hacks, and my attempts to analogize modular synthesizers into everyday life.

Opinions are strictly my own.

Let’s connect