Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Convert CBR Comics to CBZ, with Optional WebP Pages

Turn a folder of CBR comics into CBZ with 7-Zip, and optionally shrink the pages to WebP with cwebp or ImageMagick, testing every file before anything is replaced.

AT A GLANCEConvert-ComicArchive.ps1
What it does
Extracts each .cbr with 7-Zip, optionally converts JPEG and PNG pages to WebP with cwebp or ImageMagick, and repacks everything as a .cbz. Each new file is tested before the original is replaced or removed.
Requires
  • PowerShell 7+ for parallel image conversion (Windows PowerShell 5.1 works, one page at a time)
  • 7-Zip (7z.exe)
  • For WebP only, cwebp (from Google's libwebp tools) or ImageMagick 7 (magick.exe)
Permissions
Read and write access to your comics folder. No admin rights.
Runs on
Windows 10/11
Tested
Parse-checked and run against test CBR/CBZ files with 7-Zip 23.01, cwebp 1.3.2 and ImageMagick in PowerShell 7.4, including -WhatIf, a corrupt CBR and re-running over already-converted files

Part 4 of the thread Archive conversion workshop

CBR and CBZ are the same idea, a stack of page images in an archive, with one difference: CBR is RAR inside and CBZ is ZIP. ZIP is open, every reader and server supports it, and tools like Komga, Kavita and Calibre are happier with it. So most people end up standardizing on CBZ.

The old version of this post leaned on nConvert and a hard-coded 12 CPU cores, and had a bug where its "do you want WebP?" prompt was ignored: -match "" is true for any answer, so it converted everything no matter what you typed. It also deleted each CBR before it had built the replacement. This rewrite fixes all of that. 7-Zip does the extracting (it reads RAR and RAR5 fine), and if you want smaller files, you choose the WebP converter with -ImageConverter: cwebp from Google's libwebp tools, or ImageMagick's magick.

It's the last part of the archive series, and it borrows the same safety habits as the others: build the new file in a temp folder, test it, then swap it in.

Convert-ComicArchive.ps1Download
<#
.SYNOPSIS
    Converts CBR comics to CBZ, with optional JPEG/PNG to WebP recompression.
.DESCRIPTION
    Uses 7-Zip to extract each comic (RAR or ZIP inside, 7-Zip doesn't care), optionally runs
    every page through cwebp or ImageMagick to make WebP images, then repacks the pages as a
    stored (uncompressed) ZIP with a .cbz extension. The new file is tested before anything
    is replaced or removed. Point it at .cbz files with -Include to shrink an existing library.
    Supports -WhatIf.
.PARAMETER Path
    Comic files, or folders to search. Accepts pipeline input.
.PARAMETER Include
    Extensions to process. Default .cbr. Add .cbz to recompress existing CBZ files (needs -ImageConverter).
.PARAMETER Recurse
    Search subfolders when Path is a folder.
.PARAMETER ImageConverter
    None (default) just repackages. cwebp or magick converts pages to WebP.
.PARAMETER ConverterPath
    Full path to cwebp.exe or magick.exe. If omitted, the script looks on PATH.
.PARAMETER Quality
    WebP quality, 1-100. Default 80.
.PARAMETER ThrottleLimit
    How many images to convert at once on PowerShell 7. Default: number of logical CPUs.
.PARAMETER RemoveSource
    Delete each .cbr after its .cbz has been built and tested.
.PARAMETER Force
    Overwrite a .cbz that already exists next to a .cbr.
.PARAMETER SevenZipPath
    Full path to 7z.exe. If omitted, the script checks PATH, then the usual Program Files folders.
.EXAMPLE
    .\Convert-ComicArchive.ps1 -Path D:\Comics -Recurse -WhatIf
.EXAMPLE
    .\Convert-ComicArchive.ps1 -Path D:\Comics -Recurse -Include .cbr, .cbz -ImageConverter cwebp -Quality 82 -RemoveSource
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('FullName')]
    [string[]]$Path = '.',
    [ValidateSet('.cbr', '.cbz')][string[]]$Include = '.cbr',
    [switch]$Recurse,
    [ValidateSet('None', 'cwebp', 'magick')][string]$ImageConverter = 'None',
    [string]$ConverterPath,
    [ValidateRange(1, 100)][int]$Quality = 80,
    [ValidateRange(1, 64)][int]$ThrottleLimit = [Environment]::ProcessorCount,
    [switch]$RemoveSource,
    [switch]$Force,
    [string]$SevenZipPath
)

begin {
    if (-not $SevenZipPath) {
        $SevenZipPath = @(
            Get-Command -Name 7z.exe, 7z -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
            if ($env:ProgramFiles) { Join-Path $env:ProgramFiles '7-Zip\7z.exe' }
            if (${env:ProgramFiles(x86)}) { Join-Path ${env:ProgramFiles(x86)} '7-Zip\7z.exe' }
        ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1
    }
    if (-not $SevenZipPath -or -not (Test-Path -LiteralPath $SevenZipPath)) { throw '7-Zip not found. Install it from 7-zip.org or pass -SevenZipPath.' }

    if ($ImageConverter -ne 'None') {
        if (-not $ConverterPath) { $ConverterPath = Get-Command -Name $ImageConverter -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source }
        if (-not $ConverterPath -or -not (Test-Path -LiteralPath $ConverterPath)) { throw "$ImageConverter not found. Install it or pass -ConverterPath." }
    }
    elseif ($Include -contains '.cbz') { throw 'Re-processing .cbz files only makes sense with -ImageConverter cwebp or magick.' }

    function Invoke-SevenZip([string[]]$Arguments) {
        $out = & $SevenZipPath @Arguments 2>&1
        if ($LASTEXITCODE -ge 2) {
            $errText = $out | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | ForEach-Object { "$_".Trim() } | Where-Object { $_ -match '\w' -and $_ -notmatch 'RemoteException|^ERRORS:$' -and $_ -ne $Arguments[-1] }
            throw "7-Zip exit code ${LASTEXITCODE}: $(($errText | Select-Object -Unique) -join '; ')"
        }
    }

    # One page in, one .webp out. Kept as text so PowerShell 7 can rebuild it inside -Parallel.
    $convertPage = @'
param($Exe, $Kind, $Quality, $Source)
$dest = [IO.Path]::ChangeExtension($Source, '.webp')
if ($Kind -eq 'cwebp') { & $Exe -quiet -q $Quality $Source -o $dest 2>$null } else { & $Exe $Source -quality $Quality $dest 2>$null }
if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $dest)) { Remove-Item -LiteralPath $Source; 1 } else { Remove-Item -LiteralPath $dest -ErrorAction SilentlyContinue; 0 }
'@
}

process {
    $comics = foreach ($item in $Path) {
        if (Test-Path -LiteralPath $item -PathType Container) {
            Get-ChildItem -LiteralPath $item -File -Recurse:$Recurse | Where-Object { $Include -contains $_.Extension.ToLower() }
        }
        else { Get-Item -LiteralPath $item }
    }

    foreach ($comic in $comics) {
        $target  = Join-Path $comic.DirectoryName ($comic.BaseName + '.cbz')
        $inPlace = $comic.Extension -eq '.cbz'
        $result  = [ordered]@{ Name = $comic.Name; BeforeKB = [math]::Round($comic.Length / 1KB); AfterKB = $null; Pages = $null; WebP = 0; Status = $null }

        if (-not $inPlace -and (Test-Path -LiteralPath $target) -and -not $Force) { $result.Status = 'SkippedExists'; [pscustomobject]$result; continue }
        $action = if ($inPlace) { 'Recompress pages and replace' } else { "Convert to $target" }
        if (-not $PSCmdlet.ShouldProcess($comic.FullName, $action)) { continue }

        $work = Join-Path ([IO.Path]::GetTempPath()) ('comic-' + [guid]::NewGuid().ToString('N'))
        try {
            $pagesDir = Join-Path $work 'pages'
            $null = New-Item -ItemType Directory -Path $pagesDir -Force
            Invoke-SevenZip @('x', '-y', '-bd', "-o$pagesDir", $comic.FullName)

            $allFiles = @(Get-ChildItem -LiteralPath $pagesDir -File -Recurse)
            $images = @($allFiles | Where-Object { '.jpg', '.jpeg', '.png' -contains $_.Extension.ToLower() })
            $result.Pages = @($allFiles | Where-Object { '.jpg', '.jpeg', '.png', '.webp', '.gif' -contains $_.Extension.ToLower() }).Count
            if (-not $allFiles) { throw 'Archive is empty.' }
            if ($inPlace -and -not $images) { $result.Status = 'NothingToConvert'; [pscustomobject]$result; continue }

            if ($ImageConverter -ne 'None' -and $images) {
                if ($PSVersionTable.PSVersion.Major -ge 7) {
                    $done = $images.FullName | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
                        & ([scriptblock]::Create($using:convertPage)) $using:ConverterPath $using:ImageConverter $using:Quality $_
                    }
                }
                else {
                    $sb = [scriptblock]::Create($convertPage)
                    $done = foreach ($img in $images.FullName) { & $sb $ConverterPath $ImageConverter $Quality $img }
                }
                $result.WebP = ($done | Measure-Object -Sum).Sum
                if ($result.WebP -lt $images.Count) { Write-Warning "$($comic.Name): $($images.Count - $result.WebP) page(s) kept in their original format." }
            }

            # Pages are already compressed, so store them (-mx=0). Faster, and readers open it instantly.
            $tempCbz = Join-Path $work 'out.cbz'
            Invoke-SevenZip @('a', '-tzip', '-mx=0', '-mcu=on', '-y', '-bd', $tempCbz, (Join-Path $pagesDir '*'))
            Invoke-SevenZip @('t', '-bd', $tempCbz)

            Move-Item -LiteralPath $tempCbz -Destination $target -Force -WhatIf:$false
            $result.AfterKB = [math]::Round((Get-Item -LiteralPath $target).Length / 1KB)
            $result.Status = if ($inPlace) { 'Recompressed' } else { 'Converted' }

            if (-not $inPlace -and $RemoveSource -and $PSCmdlet.ShouldProcess($comic.FullName, 'Delete original CBR')) {
                Remove-Item -LiteralPath $comic.FullName -Force
                $result.Status = 'ConvertedRemovedCbr'
            }
        }
        catch {
            $result.Status = "Failed: $($_.Exception.Message)"
            Write-Warning "$($comic.Name): $($_.Exception.Message)"
        }
        finally {
            Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue -WhatIf:$false
        }
        [pscustomobject]$result
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Pathstring[].Comic files, or folders to search. Takes pipeline input.
-Includestring[].cbrWhich extensions to process. Add .cbz to recompress an existing CBZ library (needs -ImageConverter).
-Recurseswitch—Search subfolders too. You probably want this for a library organized by series.
-ImageConverterstringNoneNone just repackages. cwebp or magick converts JPEG and PNG pages to WebP.
-ConverterPathstring—Full path to cwebp.exe or magick.exe if they're not on your PATH.
-Qualityint80WebP quality, 1 to 100. 75 to 85 is the sweet spot for scanned pages.
-ThrottleLimitintCPU countHow many pages to convert at once on PowerShell 7.
-RemoveSourceswitch—Delete each .cbr after its .cbz has been built and tested. Honors -WhatIf.
-Forceswitch—Overwrite a .cbz that already sits next to a .cbr with the same name.
-SevenZipPathstring—Full path to 7z.exe. Leave it off and the script checks PATH, then Program Files and Program Files (x86).

Run it

See what would be converted, without touching anything.

.\Convert-ComicArchive.ps1 -Path D:\Comics -Recurse -WhatIf

Straight repackage, CBR to CBZ, and clean up the CBRs.

.\Convert-ComicArchive.ps1 -Path D:\Comics -Recurse -RemoveSource

Convert and shrink everything, including CBZs you already have, with cwebp.

.\Convert-ComicArchive.ps1 -Path D:\Comics -Recurse -Include .cbr, .cbz -ImageConverter cwebp -Quality 80 -RemoveSource

Use ImageMagick instead, and leave a couple of cores free.

.\Convert-ComicArchive.ps1 -Path 'D:\Comics\Space Rangers' -ImageConverter magick -ThrottleLimit 4

What you'll see

Example outputvalues are illustrative
WARNING: Space Rangers 007.cbr: 7-Zip exit code 2: ERROR: D:\Comics\Space Rangers 007.cbr; Cannot open the file as archive

Name                    BeforeKB AfterKB Pages WebP Status
----                    -------- ------- ----- ---- ------
Space Rangers 005.cbr      48213   21870    24   24 ConvertedRemovedCbr
Space Rangers 006.cbr      51002   23114    26   26 ConvertedRemovedCbr
Space Rangers 007.cbr        512                  0 Failed: 7-Zip exit code 2: ...
Space Rangers 004.cbz      46120   20988    24   24 Recompressed
Space Rangers 003.cbz      19870             22    0 NothingToConvert

How it works

  1. Check the tools. It finds 7-Zip the same way as the rest of the series. If you picked a WebP converter, it finds that too (or uses -ConverterPath), and refuses to start without it.
  2. Extract each comic to a temp folder. 7-Zip handles RAR, RAR5 and mislabeled ZIPs alike, and keeps any subfolders so page order survives.
  3. Convert pages, if asked. Every .jpg, .jpeg and .png goes through cwebp (-q <quality>) or magick (-quality <quality>). On PowerShell 7 that runs in parallel with ForEach-Object -Parallel; on 5.1 it runs one page at a time. A page that fails to convert keeps its original format, and you get a warning with the count.
  4. Repack and test. Pages are zipped with -mx=0 -mcu=on into a new .cbz, then 7z t checks it.
  5. Replace or remove. The tested CBZ is moved into place. For a CBR, -RemoveSource deletes the original through ShouldProcess. For a CBZ being recompressed, the new file replaces the old one, and -WhatIf shows that before it happens.

Take it further

  • Resize while you're at it. cwebp's -resize 1600 0 or magick's -resize 1600x> shrink oversized scans to a sensible reading width, which saves far more than quality tweaks do.
  • Try lossless for line art. Clean black-and-white pages can come out smaller with cwebp's -lossless than with lossy settings, and they stay pixel-perfect.
  • Point your library server at it afterwards. If you run Komga or Kavita, trigger a library scan once the conversion's done so it picks up the new files.

Things that'll trip you up

  • Check your reader handles WebP first. Most current desktop readers and servers do, but some older apps and a lot of e-ink devices don't. Convert one issue, open it everywhere you read, then do the rest.
  • WebP is lossy, and there's no going back. Once the JPEGs are gone, they're gone. If you care about the originals, run with -ImageConverter None first, keep a backup of the library, or skip -RemoveSource until you've spot-checked a few.
  • Plenty of .cbr files are really ZIPs. Somebody renamed a .zip to .cbr years ago, and it spread. 7-Zip looks at the file's contents, not the extension, so those convert fine.
  • ImageMagick 6 doesn't have magick.exe. The -ImageConverter magick option expects ImageMagick 7, where the command is magick. If you only have the older convert.exe, pass it with -ConverterPath; the arguments are the same.
  • Pages are stored, not compressed. The CBZ is built with -mx=0 on purpose. JPEG and WebP are already compressed, so zipping them again just costs time, and stored files open faster in readers.