Button: cbr to cbz

Hello, world!

Disclaimer & Invitation for Feedback
Although I've experimented with script add-ins and rename scripts, this is my first foray into script functions, so the way I've used the API is probably naive. Besides, my knowledge of JS is ankle-deep. I welcome all criticisms!

The button is attached. A natural home for it would be the archive pull-down menu.
Select one or more files and click.

Everyone has their favorite comic book format. I don't love cbr because fewer people have the tools to manipulate RARs. So I like to convert everything to cbz.

Some design choices are explained in the comments at the top of the code.

The image files are renamed. The script assumes that AddFilesFromFolder passes files sorted by name_stem to the Rename command. This seems to work. If not, the order will be wrong in the cbz!


CBR to CBZ.dcf (11.5 KB)

// ===         CBR to CBZ
// ===         This script converts cbr files to cbz
// ===
// ===         Design choices you should be aware of:
// ===         1. File names in a cbr can be long and unwieldy.
// ===            For compatibility with more devices, files are serially renamed:
// ===            0001.jpg 0002.jpg etc
// ===         2. When the CRB contains a folder that contains files (as opposed to just files),
// ===            the structure is simplified:
// ===            the resulting CBZ only contains the files.
// ===         3. There might be a way to go straight from cbr to cbz without a temp folder,
// ===            but what I tried told me "operation not supported by this VFS". Besides,
// ===            we're leaving the temp folders in case of failure so users can troubleshoot.

// Script entry point. DO passes the clickData object to the script.
function OnClick(clickData) {

    // While developing the script, clear the output log to identify new messages
    // The DOpus object is available to all scripts
    // DOpus.ClearOutput()

    // Note: apart from selected_files, the Tab object has selected_dirs and many more goodies
    var selected_files = clickData.func.sourcetab.selected_files

    // Instantiate dialog object -- displayed later with dlg.Show()
    // Not using var so it will be available in the global scope
    dlg = clickData.func.Dlg
    dlg.buttons = "Got it."

    // Create an enumerator object for the files selected in the tab
    // JS enumerators: https://msdn.microsoft.com/en-us/library/6ch9zb09(v=vs.94).aspx
    var enumFiles = new Enumerator(selected_files);

    // Enumerate the files selected in the tab
    while ( ! enumFiles.atEnd() ) {
        var currentFile = enumFiles.item()
        if (currentFile.ext.toLowerCase() == '.cbr') {
        else {
            dlg.message = "Not a cbr: " + currentFile.name

function cbr_to_cbz(cbrFile) {
    var cwd = cbrFile.path

    // For extraction, create tmp directory with a unique name
    // If we're creating a tmp folder based on the stem, beware of dots
    var noDots = cbrFile.name_stem.replace(".", "_")
    var timestamp = new Date().getTime()
    var tmpDirName = noDots + timestamp.toString()

    // Simple Opus commands can be run with RunCommand
    // For more complex ones, add properties then use Run
    var cmd = DOpus.Create.Command()
    cmd.RunCommand("CreateFolder " + '"' + tmpDirName + '"' + " NOSEL")

    // Extract to the new folder
    var dest = cwd + "\\" + tmpDirName
    cmd.AddLine("Copy EXTRACT")

    // The FSUtil object provides several utility methods for dealing with the file system.
    var util = DOpus.FSUtil()

    // Let's look at the extraction folder
    var extracted = util.ReadDir(dest)
    var dirFileCount = countDirsAndFiles(extracted)
    var numDir = dirFileCount[0]
    var numFiles = dirFileCount[1]
    var dirName = dirFileCount[2]

    // We can handle one directory in the cbr but not more, and not a mix
    if(numDir > 1 || (numDir == 1 && numFiles > 0) || (numDir == 0 && numFiles == 0) ) {
        dlg.message = "The file " + cbrFile +
                      " has a structure we don't know how to handle. It contains " +
                      numDir.toString() + " folder(s) and " +
                      numFiles.toString() + " file(s)." +
                      "\n\nPlease inspect the extracted folder:\n" + tmpDirName
        dlg.icon = "info"

    // Still here?
    // If there's one folder, dig deeper
    // Reset the command

    if(numDir == 1) {
        // Do we have subfolders? Let's inspect the one folder.
        extracted = util.ReadDir(dirName)
        dirFileCount = countDirsAndFiles(extracted)
        numDir = dirFileCount[0]
        numFiles = dirFileCount[1]
        if(numDir > 0 || numFiles == 0) {
            dlg.message = "The file " + cbrFile +
                          " has a structure we don't know how to handle. It contains " +
                          "a folder within a folder, or a folder with no files." +
                          "\n\nPlease inspect the extracted folder:\n" + tmpDirName
            dlg.icon = "info"
        else {
            // Add the files to be zipped.
            var folderToZip = dirName
    else {
        // All files, no folders.
        // Add the files to be zipped.
        var folderToZip = dest


    // Renaming the files by serializing the image. This averts errors with some readers.
    // Compute the number of zeroes for the rename preset based on the number of files.
    // !!! We're trusting AddFilesFromFolder to pass the files sorted by name_stem
    // !!! This is what it seems to do, and what we need for the order to be preserved in the cbz
    var numZeroes = numFiles.toString().length -1
    // for some reason str.repeat() is not recognized in this JS
    var zeroes = numZeroes == 0 ? '' : Array(numZeroes + 1).join("0")
    cmd.AddLine('Rename PATTERN ^(.*)\\.([^.]+)$ TO "[#].\\2" REGEXP NUMBER ' +
                zeroes + '1 BY 1 CASE=lower')
    cmd.AddLine("Copy ARCHIVE CREATEFOLDER=" + '"' + cbrFile.name_stem + ".cbz" + '"')

    // Delete the temporary folders
    // The doc says we might have to close the enumerator before deleting
    cmd.AddLine("Delete NORECYCLE QUIET")


function countDirsAndFiles(coll) {
// Counts folders in a collection
    var numDirs = 0
    var numFiles = 0
    var dirName = ''
    while ( ! coll.complete ) {
        var currentFile = coll.Next()
        if(currentFile.is_dir) {
            // If we have only one folder, we'll want its name
            dirName = currentFile
        else { numFiles++ }
    return [numDirs, numFiles, dirName]
1 Like


This is great, but one thing is missing - resizing the files to match desired resolution hence reducing the comic book size. :wink: For example I resize them to 800×1280 so they are still small and the resolution is still very readable for a tablet.

Currently I do it with the ComicRack, but this software is slow and lately it gave me some I/O error whilst converting so you need to leave it alone to do it. That's why I started thinking about DO scripting it myself and came to this thread. :slight_smile: So - maybe you may add this option since you've come that far?

Thanks once more @playful. Your script has enabled me to cut short through the DOpus objects woods, as I am total beginner in it, just as I am in JScript, so I converted it to more familiar VBScript. :slight_smile:

So, in the meantime I have managed to resize the images from within the script. I still have to resolve the folder-within-folder issue that is very common for the comic books, but this does not bother me too much at this point.

On the contrary, the cb7 archiving is still falling short and it is still giving me VFS error for some reason. Until this is settled, I'm afraid I'll need to use brute force: 7zip executable to form 7z and rename it to cb7 afterwards:

This does NOT work:
Copy ARCHIVE CREATEFOLDER=" & cFile.name_stem + ".cb7"

And this DOES work:
Copy ARCHIVE CREATEFOLDER=" & cFile.name_stem + ".cbz"

I'll share my solution shortly upon final resolution.

One more thanks @playful . Here is my solution: Comic Book CBx-To-CBx Convert & Resize v1.40