Skip to main content
Stephen Marsh
Community Expert
Community Expert
August 17, 2025
Question

Batch Save As AVIF script

  • August 17, 2025
  • 1 reply
  • 399 views

I have created a "Batch Save As AVIF" script, based on my "Batch Save As WebP" script.

 

Key Features:

* Version Check - Requires Photoshop 2025+ for native AVIF export support.

* Non-Destructive Workflow - Original source files remain unchanged.

* Folder Selection with Recursion - Optionally process subfolders.

* Mirror Folder Structure - Retain the original folder layout in the output.

* Compression Settings - Choose Lossy or Lossless, with adjustable quality (0–100).

* Resize by Longest Edge & PPI - Proportionally scale by longest edge and/or set resolution metadata.

* Automatic RGB Conversion - Converts non-RGB modes (CMYK, LAB, Bitmap, Indexed) to 8-bit RGB.

* sRGB Profile Option - Convert to sRGB IEC61966-2.1 or keep current color profile.

* Metadata Toggles - Include/exclude XMP, EXIF, or Photoshop metadata.

* Action Runner - Apply Photoshop actions during batch processing.
 
 
/*
Batch Save As AVIF scriptUI GUI v1-0.jsx
Stephen Marsh
https://community.adobe.com/t5/photoshop-ecosystem-discussions/...
v1.0 - 30th July 2025: Initial release

* Version Check - Requires Photoshop 2025+ for native AVIF export support.  
* Non-Destructive Workflow - Keeps original files unchanged.  
* Folder Selection with Recursion - Optionally processes subfolders.  
* Mirror Folder Structure - Retains original folder layout in output.  
* Compression Settings - Choose lossy or lossless for size/quality control.  
* Resize by Longest Edge & PPI - Scales proportionally and or sets resolution metadata.  
* Auto RGB Conversion - Ensure that non-RGB colour modes are converted to RGB.  
* sRGB Profile Option - Standardizes color output.  
* Metadata Toggles - Include/exclude XMP, EXIF, or PSD data.  
* Action Runner - Applies Photoshop actions during batch.

Based on:
Batch Save As WebP scriptUI GUI v1-9.jsx
https://community.adobe.com/t5/photoshop-ecosystem-discussions/export-many-files-at-once-in-webp-format-photoshop/td-p/13604411/page/4#U14859793
*/

#target photoshop

// Ensure that version 2025 or later is being used
var versionNumber = app.version.split(".");
var versionCheck = parseInt(versionNumber[0]);
if (versionCheck < 26) {
    alert("You must use Photoshop 2025 or later to save using native AVIF format...");

} else {
    // Create the ScriptUI dialog window
    var theDialogWin = new Window("dialog", "AVIF Batch Processor (v1.0)");

    theDialogWin.orientation = "column";
    theDialogWin.alignChildren = "left";

    // Input Folder Section
    var inputFolderPanel = theDialogWin.add("panel", undefined, "Input Folder");
    inputFolderPanel.orientation = "column";
    inputFolderPanel.alignChildren = ["fill", "top"];
    inputFolderPanel.alignment = ["fill", "top"];

    inputFolderPanel.add("statictext", undefined, "Select the source image folder to process:");
    var inputFolderRow = inputFolderPanel.add("group");
    inputFolderRow.orientation = "row";
    inputFolderRow.alignment = ["fill", "top"];

    var inputFolderButton = inputFolderRow.add("button", undefined, "Browse...");
    inputFolderButton.alignment = ["left", "top"];

    var inputFolderText = inputFolderRow.add("edittext", undefined, "");
    inputFolderText.characters = 30;
    inputFolderText.alignment = ["fill", "top"];

    // Process Subfolders Checkbox
    var recursiveCheckbox = inputFolderPanel.add("checkbox", undefined, "Include all subfolders");
    recursiveCheckbox.value = false;

    // Output Folder Section
    var outputFolderPanel = theDialogWin.add("panel", undefined, "Output Folder");
    outputFolderPanel.orientation = "column";
    outputFolderPanel.alignChildren = ["fill", "top"];
    outputFolderPanel.alignment = ["fill", "top"];

    outputFolderPanel.add("statictext", undefined, "Select the location to save the AVIF files:");
    var outputFolderRow = outputFolderPanel.add("group");
    outputFolderRow.orientation = "row";
    outputFolderRow.alignment = ["fill", "top"];

    var outputFolderButton = outputFolderRow.add("button", undefined, "Browse...");
    outputFolderButton.alignment = ["left", "top"];

    var outputFolderText = outputFolderRow.add("edittext", undefined, "");
    outputFolderText.characters = 30;
    outputFolderText.alignment = ["fill", "top"];

    // Mirror Directory Structure Checkbox
    var mirrorStructureCheckbox = outputFolderPanel.add("checkbox", undefined, "Retain the source subfolder structure");
    mirrorStructureCheckbox.value = false;
    mirrorStructureCheckbox.enabled = false;

    // Enable/Disable "Mirror Structure" based on the recursive option
    recursiveCheckbox.onClick = function () {
        mirrorStructureCheckbox.enabled = recursiveCheckbox.value;
    };

    // Create a panel for the compression settings
    var compressionPanel = theDialogWin.add("panel", undefined, "Compression Settings");
    compressionPanel.orientation = "column";
    compressionPanel.alignChildren = ["left", "top"];
    compressionPanel.margins = 15;
    compressionPanel.alignment = ["fill", "top"];

    var mainGroup = compressionPanel.add("group");
    mainGroup.orientation = "row";
    mainGroup.alignChildren = ["left", "center"];
    mainGroup.alignment = ["fill", "top"];
    mainGroup.spacing = 10;

    mainGroup.add("statictext", undefined, "Compression Type:");
    var compTypeDropdown = mainGroup.add("dropdownlist", undefined, ["Lossy", "Lossless"]);
    compTypeDropdown.selection = 0;

    mainGroup.add("statictext", undefined, "Compression Quality:");

    var qualityGroup = mainGroup.add("group");
    qualityGroup.orientation = "row";
    qualityGroup.alignChildren = ["left", "center"];
    qualityGroup.spacing = 10;

    var compValueSlider = qualityGroup.add("slider", undefined, 50, 0, 100);
    compValueSlider.size = [281, 20];
    var compValueText = qualityGroup.add("edittext", undefined, "50");
    compValueText.characters = 4;

    // Update the text while dragging slider
    compValueSlider.onChanging = function () {
        compValueText.text = Math.round(this.value).toString();
    };

    // Update the text when slider changes via keyboard or release
    compValueSlider.onChange = function () {
        compValueText.text = Math.round(this.value).toString();
    };

    // Update the slider when text changes
    compValueText.onChange = function () {
        var inputValue = parseInt(this.text, 10);
        if (!isNaN(inputValue) && inputValue >= 0 && inputValue <= 100) {
            compValueSlider.value = inputValue;
        } else {
            this.text = Math.round(compValueSlider.value).toString();
        }
    };

    // Function to toggle the slider based on compression type
    function updateCompressionControls() {
        var enableControls = compTypeDropdown.selection.text !== "Lossless";
        compValueSlider.enabled = enableControls;
        compValueText.enabled = enableControls;
    }

    updateCompressionControls();

    compTypeDropdown.onChange = updateCompressionControls;

    // Create a panel for all checkboxes
    var checkboxPanel = theDialogWin.add("panel", undefined, "Options");
    checkboxPanel.orientation = "column";
    checkboxPanel.alignChildren = ["left", "top"];
    checkboxPanel.margins = 15;
    checkboxPanel.alignment = ["fill", "top"];

    var checkboxGroup = checkboxPanel.add("group");
    checkboxGroup.orientation = "row";
    checkboxGroup.alignChildren = ["left", "top"];
    checkboxGroup.alignment = ["left", "top"];
    checkboxGroup.spacing = 20;

    // Left column
    var leftColumn = checkboxGroup.add("group");
    leftColumn.orientation = "column";
    leftColumn.alignChildren = ["left", "top"];

    var fitImageGroup = leftColumn.add("group");
    fitImageGroup.orientation = "row";
    fitImageGroup.alignChildren = ["left", "center"];
    fitImageGroup.alignment = ["fill", "top"];
    var fitImageCheckbox = fitImageGroup.add("checkbox", undefined, "Fit Longest Edge (px):");
    fitImageCheckbox.value = false;
    var fitImageInput = fitImageGroup.add("editnumber", undefined, "1920");
    fitImageInput.helpTip = "Proportionally resize the longest edge of the image to the specified value";
    fitImageInput.characters = 5;
    fitImageInput.enabled = fitImageCheckbox.value;
    fitImageCheckbox.onClick = function () {
        fitImageInput.enabled = fitImageCheckbox.value;
    };

    var xmpDataCheckbox = leftColumn.add("checkbox", undefined, "Include XMP Data");
    xmpDataCheckbox.value = false;

    // Middle column
    var middleColumn = checkboxGroup.add("group");
    middleColumn.orientation = "column";
    middleColumn.alignChildren = ["fill", "top"];

    var ppiGroup = middleColumn.add("group");
    ppiGroup.orientation = "row";
    ppiGroup.alignChildren = ["left", "center"];
    ppiGroup.alignment = ["left", "top"];
    var ppiCheckbox = ppiGroup.add("checkbox", undefined, "PPI Value:");
    ppiCheckbox.value = false;
    var ppiInput = ppiGroup.add("editnumber", undefined, "300");
    ppiInput.characters = 5;
    ppiInput.enabled = ppiCheckbox.value;
    ppiCheckbox.onClick = function () {
        ppiInput.enabled = ppiCheckbox.value;
    };

    var exifDataCheckbox = middleColumn.add("checkbox", undefined, "Include EXIF Data");
    exifDataCheckbox.value = false;

    // Right column
    var rightColumn = checkboxGroup.add("group");
    rightColumn.orientation = "column";
    rightColumn.alignChildren = ["fill", "top"];

    var psDataCheckbox = rightColumn.add("checkbox", undefined, "Include Photoshop Data");
    psDataCheckbox.value = false;

    var rgbGroup = checkboxPanel.add("group");
    rgbGroup.orientation = "row";
    rgbGroup.alignChildren = ["left", "center"];
    rgbGroup.spacing = 10;
    rgbGroup.add("statictext", undefined, "RGB Profile Conversion:");
    var rgbConversionDropdown = rgbGroup.add("dropdownlist", undefined, ["Convert to sRGB space", "Keep Current RGB space"]);
    rgbConversionDropdown.selection = 0;

    // Create a panel to contain the action dropdowns
    var actionPanel = theDialogWin.add("panel", undefined, "Run Action");
    actionPanel.orientation = "column";
    actionPanel.alignChildren = ["fill", "top"];
    actionPanel.margins = 15;
    actionPanel.alignment = ["fill", "top"];

    var dropdownWidth = 350;

    var actionGroup = actionPanel.add("group");
    actionGroup.orientation = "row";
    actionGroup.alignChildren = ["left", "top"];
    actionGroup.alignment = ["fill", "top"];

    var actionSetColumn = actionGroup.add("group");
    actionSetColumn.orientation = "column";
    actionSetColumn.alignChildren = ["left", "top"];
    actionSetColumn.alignment = ["fill", "top"];

    var actionSetDropdown = actionSetColumn.add('dropdownlist', undefined, []);
    actionSetDropdown.preferredSize.width = dropdownWidth;

    var actionLabelColumn = actionGroup.add("group");
    actionLabelColumn.orientation = "column";
    actionLabelColumn.alignChildren = ["left", "top"];
    actionLabelColumn.alignment = ["fill", "top"];

    var actionDropdown = actionLabelColumn.add('dropdownlist', undefined, []);
    actionDropdown.preferredSize.width = dropdownWidth;

    actionSetDropdown.onResizing = actionSetDropdown.onResize = function () {
        this.size.width = dropdownWidth;
    };
    actionDropdown.onResizing = actionDropdown.onResize = function () {
        this.size.width = dropdownWidth;
    };

    // Populate the action set dropdown
    actionSetDropdown.add('item', '');
    var actionSets = getActionSets();
    for (var i = 0; i < actionSets.length; i++) {
        actionSetDropdown.add('item', actionSets[i]);
    }
    actionSetDropdown.selection = actionSetDropdown.items[0];
    actionDropdown.add('item', '');

    actionSetDropdown.onChange = function () {
        actionDropdown.removeAll();

        if (actionSetDropdown.selection && actionSetDropdown.selection.text != '') {
            var actions = getActions(actionSetDropdown.selection.text);
            for (var i = 0; i < actions.length; i++) {
                actionDropdown.add('item', actions[i]);
            }
            if (actions.length > 0) {
                actionDropdown.selection = actionDropdown.items[0];
            }
        } else {
            actionDropdown.add('item', '');
            actionDropdown.selection = actionDropdown.items[0];
        }
    };

    var buttonGroup = theDialogWin.add("group");
    buttonGroup.alignment = "right";
    var cancelButton = buttonGroup.add("button", undefined, "Cancel");
    var okButton = buttonGroup.add("button", undefined, "OK");

    inputFolderButton.onClick = function () {
        var inputFolder = Folder.selectDialog("Select the input folder:");
        if (inputFolder) {
            inputFolderText.text = inputFolder.fsName;
            if (!outputFolderText.text) {
                outputFolderText.text = inputFolder.fsName;
            }
        }
    };

    outputFolderButton.onClick = function () {
        var outputFolder = Folder.selectDialog("Select the output folder:");
        if (outputFolder) {
            outputFolderText.text = outputFolder.fsName;
        }
    };

    cancelButton.onClick = function () {
        theDialogWin.close();
    };

    okButton.onClick = function () {
        theDialogWin.close();
        app.refresh();
        app.togglePalettes();
        if (inputFolderText.text === "" || outputFolderText.text === "") {
            alert("Ensure that you select both input and output folders and other options before pressing OK!");
            app.togglePalettes();
            return;
        }

        var inputFolder = new Folder(inputFolderText.text);
        var outputFolder = new Folder(outputFolderText.text);

        var compType = compTypeDropdown.selection.text === "Lossless" ? "compressionLossless" : "compressionLossy";
        var compValue = Math.round(compValueSlider.value);
        var xmpData = xmpDataCheckbox.value;
        var exifData = exifDataCheckbox.value;
        var psData = psDataCheckbox.value;
        var rgbConversion = rgbConversionDropdown.selection.text;

        var fitValue = fitImageCheckbox.value ? parseInt(fitImageInput.text) : null;
        var ppi = ppiCheckbox.value ? parseInt(ppiInput.text) : null;

        processFiles(inputFolder, outputFolder, compType, compValue, xmpData, exifData, psData, fitValue, ppi, recursiveCheckbox.value, mirrorStructureCheckbox.value, rgbConversion);
        theDialogWin.close();
    };

    theDialogWin.show();
}


// Main processing function with optional recursion and mirroring structure
function processFiles(inputFolder, outputFolder, compType, compValue, xmpData, exifData, psData, fitValue, ppi, recursive, mirrorStructure, rgbConversion) {
    var fileList = getFilesRecursive(inputFolder, recursive);
    fileList.sort();
    var savedDisplayDialogs = app.displayDialogs;
    app.displayDialogs = DialogModes.NO;

    var inputFileCounter = 0;
    var fileCounter = 0;

    // Create the progress bar palette
    var progressBar = new Window("palette", "Processing Files");
    progressBar.preferredSize = [350, 100];
    // Status text
    var statusText = progressBar.add("statictext", undefined, "Preparing...");
    statusText.alignment = "fill";
    // Progress bar
    var progress = progressBar.add("progressbar", undefined, 0, fileList.length);
    progress.preferredSize.width = 300;
    progressBar.show();
    app.refresh();

    // Process each file
    for (var i = 0; i < fileList.length; i++) {
        // Update progress UI
        var percent = Math.round(((i + 1) / fileList.length) * 100);
        statusText.text = "Processing file " + (i + 1) + " of " + fileList.length + " (" + percent + "%)";
        progress.value = i + 1;
        app.refresh();

        // Open file
        open(fileList[i]);

        // Prepare target folder
        var relativePath = fileList[i].parent.fsName.replace(inputFolder.fsName, "");
        var targetFolder = outputFolder;
        if (mirrorStructure && relativePath !== "") {
            targetFolder = new Folder(outputFolder + "/" + relativePath);
            if (!targetFolder.exists) {
                targetFolder.create();
            }
        }

        // Mode conversion logic
        if (activeDocument.mode == DocumentMode.BITMAP) {
            activeDocument.changeMode(ChangeMode.GRAYSCALE);
            activeDocument.changeMode(ChangeMode.RGB);
            //if (rgbConversion === "Convert to sRGB space") {
            if (rgbConversionDropdown.selection.index === 0) {
                activeDocument.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false);
            }
            activeDocument.bitsPerChannel = BitsPerChannelType.EIGHT;
        } else if (activeDocument.mode == DocumentMode.INDEXEDCOLOR || activeDocument.mode == DocumentMode.CMYK || activeDocument.mode == DocumentMode.LAB) {
            activeDocument.changeMode(ChangeMode.RGB);
            //if (rgbConversion === "Convert to sRGB space") {
            if (rgbConversionDropdown.selection.index === 0) {
                activeDocument.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false);
            }
            activeDocument.bitsPerChannel = BitsPerChannelType.EIGHT;
        } else {
            activeDocument.changeMode(ChangeMode.RGB);
            //if (rgbConversion === "Convert to sRGB space") {
            if (rgbConversionDropdown.selection.index === 0) {
                activeDocument.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false);
            }
            activeDocument.bitsPerChannel = BitsPerChannelType.EIGHT;
        }

        // Run actions if selected
        if (actionSetDropdown.selection && actionSetDropdown.selection.text != '' &&
            actionDropdown.selection && actionDropdown.selection.text != '') {
            runAction(actionSetDropdown.selection.text, actionDropdown.selection.text);
        }

        // Fit image if needed
        if (fitValue !== null || ppi !== null) {
            fitImage(fitValue, ppi);
        }

        // Save & close
        saveAVIF(compType, compValue, xmpData, exifData, psData, targetFolder);
        activeDocument.close(SaveOptions.DONOTSAVECHANGES);

        // Increment the counters
        inputFileCounter++;
        fileCounter++;
    }

    // Return focus to the host and close the progress bar palette
    app.bringToFront();
    progressBar.close();

    // End of script notification
    app.displayDialogs = savedDisplayDialogs;
    app.beep();
    alert('Script completed!' + '\r' + inputFileCounter + ' source files saved as ' + fileCounter + ' AVIF files!');
    app.togglePalettes();
}


/// Helper Functions ///

function getFilesRecursive(folder, recursive) {
    var fileList = [];
    var allFiles = folder.getFiles();
    for (var i = 0; i < allFiles.length; i++) {
        var file = allFiles[i];
        if (file instanceof Folder && recursive) {
            fileList = fileList.concat(getFilesRecursive(file, recursive));
        } else if (file instanceof File && /\.(avif|webp|tif|tiff|jpg|jpeg|psd|psb|png|tga)$/i.test(file.name)) {
            fileList.push(file);
        }
    }
    return fileList;
}

function fitImage(fitValue, ppi) {
    if (fitValue !== null && ppi !== null) {
        if (activeDocument.height.value > activeDocument.width.value) {
            activeDocument.resizeImage(null, UnitValue(fitValue, "px"), ppi, ResampleMethod.BICUBIC);
        } else {
            activeDocument.resizeImage(UnitValue(fitValue, "px"), null, ppi, ResampleMethod.BICUBIC);
        }
    } else if (fitValue !== null) {
        if (activeDocument.height.value > activeDocument.width.value) {
            activeDocument.resizeImage(null, UnitValue(fitValue, "px"), activeDocument.resolution, ResampleMethod.BICUBIC);
        } else {
            activeDocument.resizeImage(UnitValue(fitValue, "px"), null, activeDocument.resolution, ResampleMethod.BICUBIC);
        }
    } else if (ppi !== null) {
        activeDocument.resizeImage(undefined, undefined, ppi, ResampleMethod.NONE);
    }
}

function saveAVIF(compType, compValue, xmpData, exifData, psData, outputFolder) {
    var exportSettings = {
        isLossy: compType === "compressionLossy",
        quality: compValue,
        outputFolder: outputFolder
    };
    var AVIFDocName = activeDocument.name.replace(/\.[^\.]+$/, '');
    try {
        function s2t(s) { return stringIDToTypeID(s); }
        var descSave = new ActionDescriptor();
        var descAVIFOptions = new ActionDescriptor();
        var idAVIFCompression = s2t("AVIFCompression");
        if (exportSettings.isLossy) {
            descAVIFOptions.putEnumerated(s2t("colorCompression"), idAVIFCompression, s2t("compressionLossy"));
            descAVIFOptions.putInteger(s2t("colorQuality"), exportSettings.quality);
        } else {
            descAVIFOptions.putEnumerated(s2t("colorCompression"), idAVIFCompression, s2t("compressionLossless"));
        }
        descAVIFOptions.putEnumerated(s2t("alphaCompression"), idAVIFCompression, s2t("compressionLossless"));
        descAVIFOptions.putEnumerated(s2t("colorFormat"), s2t("AVIFColorFormat"), s2t("AVIFFormat422")); // Options: AVIFFormat444, AVIFFormat422, AVIFFormat420
        descAVIFOptions.putEnumerated(s2t("sampleDepth"), s2t("AVIFSampleDepth"), s2t("AVIFDepth12bit"));
        descAVIFOptions.putInteger(s2t("encoderSpeed"), 6); // Options: 2 (slowest/smallest), 4, 6, 8, 10 (fastest/largest)
        descAVIFOptions.putBoolean(s2t("includeXMPData"), xmpData);
        descAVIFOptions.putBoolean(s2t("includeEXIFData"), exifData);
        descAVIFOptions.putBoolean(s2t("includePsExtras"), psData);
        descSave.putObject(s2t("as"), s2t("AVIFFormat"), descAVIFOptions);
        descSave.putPath(s2t("in"), new File(exportSettings.outputFolder + '/' + AVIFDocName + '.avif'));
        descSave.putBoolean(s2t("lowerCase"), true);
        descSave.putEnumerated(s2t("saveStage"), s2t("saveStageType"), s2t("saveSucceeded"));
        executeAction(s2t("save"), descSave, DialogModes.NO);
    } catch (e) {
        alert("Error saving AVIF:\n" + e.message + "\nLine: " + e.line);
    }
}

function getActionSets() {
    var actionSets = [];
    var i = 1;
    while (true) {
        try {
            var ref = new ActionReference();
            ref.putIndex(charIDToTypeID('ASet'), i);
            var desc = executeActionGet(ref);
            var name = desc.getString(charIDToTypeID('Nm  '));
            actionSets.push(name);
            i++;
        } catch (e) {
            break;
        }
    }
    return actionSets;
}

function getActions(actionSet) {
    var actions = [];
    var i = 1;
    while (true) {
        try {
            var ref = new ActionReference();
            ref.putIndex(charIDToTypeID('Actn'), i);
            ref.putName(charIDToTypeID('ASet'), actionSet);
            var desc = executeActionGet(ref);
            var actionName = desc.getString(charIDToTypeID('Nm  '));
            actions.push(actionName);
            i++;
        } catch (e) {
            break;
        }
    }
    return actions;
}

function runAction(actionSet, actionName) {
    var actionRef = new ActionReference();
    actionRef.putName(charIDToTypeID('Actn'), actionName);
    actionRef.putName(charIDToTypeID('ASet'), actionSet);
    var desc = new ActionDescriptor();
    desc.putReference(charIDToTypeID('null'), actionRef);
    executeAction(charIDToTypeID('Ply '), desc, DialogModes.NO);
}

 

https://prepression.blogspot.com/2017/11/downloading-and-installing-adobe-scripts.html

 

1 reply

creative explorer
Community Expert
Community Expert
August 18, 2025

@Stephen Marsh was there a question in here? Or a 'hey, I created this plugin for you?' post?

m
Stephen Marsh
Community Expert
Community Expert
August 18, 2025
quote

@Stephen Marsh was there a question in here? Or a 'hey, I created this plugin for you?' post?


By @creative explorer

 

Adobe only offer three choices for a topic:

Discussion, Idea and Bug.

 

So no, there is clearly no question. It's a pre-emptive public service announcement, usually I write scripts in response to a lamentation or request, this time there wasn't a request yet.