<# .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 } }