<#
====================================================================================
ImageMagick Utility Toolkit (magick_tools.ps1)
------------------------------------------------------------------------------------
Author : Alessandro Maestri
Version: 2.2
Date : 2025-10-31
------------------------------------------------------------------------------------
Changelog v2.2:
- extract: added --prefix and --format (png|jpg|webp) options.
- extract: default output directory now 'layers/' instead of <name>_layers.
- Usage section updated with examples.
- Fixed encoding artifacts replacing '����' with '->'.
====================================================================================
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ------------------------- CONFIG DI DEFAULT -------------------------
$DefaultMagick = "X:\ImageMagick-7.1.2-8-portable-Q16-HDRI-x64\magick.exe"
$ValidActions = 'identify', 'convert', 'resize', 'optimize', 'make-ico', 'extract'
# ------------------------- USAGE -------------------------
function Show-Usage
{
Write-Host "Usage:" -ForegroundColor Cyan
Write-Host " magick_tools.ps1 --action identify --input assets/rbackup.ico" -ForegroundColor Gray
Write-Host " magick_tools.ps1 --action convert --input logo.png --output logo.webp" -ForegroundColor Gray
Write-Host " magick_tools.ps1 --action resize --input img.png --output img_256.png --width 256" -ForegroundColor Gray
Write-Host " magick_tools.ps1 --action make-ico --input logo.png --output favicon.ico --sizes 16,32,48" -ForegroundColor Gray
Write-Host " magick_tools.ps1 --action extract --input favicon.ico --output layers --prefix icon --format webp" -ForegroundColor Gray
Write-Host "Options:" -ForegroundColor Cyan
Write-Host " --action|-Action|-a (required) one of: $( $ValidActions -join ', ' )" -ForegroundColor Gray
Write-Host " --input|-Input|-i input file" -ForegroundColor Gray
Write-Host " --output|-Output|-o output file (file for actions convert/resize/optimize/make-ico; dir for extract)" -ForegroundColor Gray
Write-Host " --width|-Width width (resize)" -ForegroundColor Gray
Write-Host " --height|-Height height (optional for resize)" -ForegroundColor Gray
Write-Host " --sizes|-Sizes size list for make-ico (e.g. 16,32,48)" -ForegroundColor Gray
Write-Host " --magick|-Magick path to magick.exe (default: $DefaultMagick)" -ForegroundColor Gray
Write-Host " --verbose|-Verbose verbose output (no value required)" -ForegroundColor Gray
Write-Host " --prefix|-Prefix filename prefix for extract output (default: layer)" -ForegroundColor Gray
Write-Host " --format|-Format output format for extract (png|jpg|webp) default: png" -ForegroundColor Gray
Write-Host " --colors|-Colors color palette size for make-ico (omit to keep truecolor + alpha)" -ForegroundColor Gray
Write-Host " --no-dither disable dithering when --colors is used" -ForegroundColor Gray
Write-Host "Available actions and examples:" -ForegroundColor Cyan
Write-Host " identify show image metadata (properties, format, dimensions)" -ForegroundColor Gray
Write-Host " Example: magick_tools.ps1 --action identify --input assets/rbackup.ico" -ForegroundColor DarkGray
Write-Host " convert convert input image to output (format inferred by output extension)" -ForegroundColor Gray
Write-Host " Example: magick_tools.ps1 --action convert --input logo.png --output logo.webp" -ForegroundColor DarkGray
Write-Host " resize resize image to width x height (height optional)" -ForegroundColor Gray
Write-Host " Example: magick_tools.ps1 --action resize --input img.png --output img_256x128.png --width 256 --height 128" -ForegroundColor DarkGray
Write-Host " optimize optimize (strip metadata, interlace Plane, quality 85%)" -ForegroundColor Gray
Write-Host " Example: magick_tools.ps1 --action optimize --input photo.jpg --output photo_opt.jpg" -ForegroundColor DarkGray
Write-Host " make-ico build multi-size .ico from input raster (requires --sizes)" -ForegroundColor Gray
Write-Host " Example: magick_tools.ps1 --action make-ico --input logo.png --output favicon.ico --sizes 16,32,48,64 --colors 256" -ForegroundColor DarkGray
Write-Host " extract extract all layers from an .ico into numbered PNG files" -ForegroundColor Gray
Write-Host " Example: magick_tools.ps1 --action extract --input favicon.ico --output layers --prefix icon --format webp" -ForegroundColor DarkGray
}
# Se nessun argomento, mostra usage e termina
if ($args.Length -eq 0)
{
Show-Usage; exit 1
}
# ------------------------- PARSER ARGOMENTI -------------------------
# Converte qualsiasi forma --key, -Key o --key=value in chiave canonicalizzata (Action, InputPath, ...) in un hashtable.
$Parsed = [ordered]@{ }
$ExpectValueFor = $null
for ($idx = 0; $idx -lt $args.Length; $idx++) {
$token = $args[$idx]
if ($ExpectValueFor) {
$Parsed[$ExpectValueFor] = $token
$ExpectValueFor = $null
continue
}
if ($token -match '^--?') {
# Gestisce forma --key=value
$pair = $token -split '=', 2
$flagToken = $pair[0]
$valueInline = if ($pair.Length -gt 1) { $pair[1] } else { $null }
$name = $flagToken -replace '^-{1,2}', ''
$canon = $null # inizializza per StrictMode
switch -Regex ( $name.ToLower()) {
'^(a|action)$' { $canon = 'Action' }
'^(i|input)$' { $canon = 'InputPath' }
'^(o|output)$' { $canon = 'OutputPath' }
'^(width)$' { $canon = 'Width' }
'^(height)$' { $canon = 'Height' }
'^(sizes)$' { $canon = 'Sizes' }
'^(magick)$' { $canon = 'Magick' }
'^(verbose)$' { $canon = 'Verbose' }
'^(prefix)$' { $canon = 'Prefix' }
'^(format)$' { $canon = 'Format' }
'^(colors)$' { $canon = 'Colors' }
'^(no-dither)$' { $canon = 'NoDither' }
default {
Write-Warning "Unknown option '$token' ignored"
}
}
if (-not $canon) {
# Opzione sconosciuta: se non inline assignment e prossimo token non è un flag, salta il valore accidentale
if ($null -eq $valueInline -and ($idx + 1) -lt $args.Length -and $args[$idx+1] -notmatch '^--?') { $idx++ }
continue
}
if ($null -ne $valueInline) {
if ($canon -eq 'Verbose') {
$Parsed[$canon] = [System.Convert]::ToBoolean($valueInline)
} else {
$Parsed[$canon] = $valueInline
}
} else {
if ($canon -eq 'Verbose') {
$Parsed[$canon] = $true
} else {
$ExpectValueFor = $canon
}
}
} else {
Write-Host "❌ Unexpected bare value '$token' (precede un'opzione?)" -ForegroundColor Red
Show-Usage; exit 1
}
}
if ($ExpectValueFor)
{
Write-Host "❌ Missing value for option '$ExpectValueFor'" -ForegroundColor Red
Show-Usage; exit 1
}
# Applica default per Magick se non passato
if (-not $Parsed.Contains('Magick'))
{
$Parsed['Magick'] = $DefaultMagick
}
if (-not $Parsed.Contains('Verbose')) { $Parsed['Verbose'] = $false }
if (-not $Parsed.Contains('Prefix')) { $Parsed['Prefix'] = 'layer' }
if (-not $Parsed.Contains('Format')) { $Parsed['Format'] = 'png' }
if (-not $Parsed.Contains('NoDither')) { $Parsed['NoDither'] = $false }
# ------------------------- VALIDAZIONE LOGICA -------------------------
function Fail
{
param([string]$Msg); Write-Host "ERROR: $Msg" -ForegroundColor Red; Show-Usage; exit 1
}
if (-not $Parsed.Contains('Action'))
{
Fail 'Missing --action'
}
$Action = $Parsed['Action']
if ($ValidActions -notcontains $Action.ToLower())
{
Fail "Invalid action '$Action'"
}
# Parametri facoltativi (rinominati per evitare conflitto con variabile automatica $input)
$InputPath = $Parsed['InputPath']
$OutputPath = $Parsed['OutputPath']
$Width = $Parsed['Width']
$Height = $Parsed['Height']
$Sizes = $Parsed['Sizes']
$Magick = $Parsed['Magick']
$Verbose = $Parsed['Verbose']
$Prefix = $Parsed['Prefix']
$Format = $Parsed['Format']
$Colors = $Parsed['Colors']
$NoDither = $Parsed['NoDither']
# Repo root (script sotto dev_tools/) per risoluzione path
$RepoRoot = Split-Path -Parent $PSScriptRoot
function Resolve-RepoPath
{
param([string]$Path); if (-not $Path)
{
return $null
}; if ( [System.IO.Path]::IsPathRooted($Path))
{
return $Path
}; Join-Path $RepoRoot $Path
}
function Invoke-InputPath
{
param([string]$Path)
if (-not $Path)
{
Fail 'Missing --input'
}
$resolved = Resolve-RepoPath $Path
if (-not (Test-Path -LiteralPath $resolved))
{
Fail "Input file not found: $Path (resolved: $resolved)"
}
return $resolved
}
function Invoke-Magick
{
param([string]$Exe)
# Se percorso esplicito esiste, usa quello
if ($Exe -and (Test-Path -LiteralPath $Exe))
{
return $Exe
}
# Tentativo ricerca in PATH (magick.exe oppure magick)
$cmd = Get-Command magick.exe -ErrorAction SilentlyContinue
if (-not $cmd)
{
$cmd = Get-Command magick -ErrorAction SilentlyContinue
}
if ($cmd)
{
Write-Host "🔍 Found magick.exe in PATH: $( $cmd.Source )" -ForegroundColor Yellow
return $cmd.Source
}
Fail "ImageMagick not found. Tried explicit path '$Exe' and PATH search (magick.exe/magick)."
}
# Risolve path eseguibile (assegna eventualmente quello trovato nel PATH)
$Magick = Invoke-Magick $Magick
$MagickVersionLine = (& $Magick -version 2>$null | Select-Object -First 1)
$IsMagick7 = $false
if ($MagickVersionLine -and ($MagickVersionLine -match 'ImageMagick 7')) { $IsMagick7 = $true }
$actionLower = $Action.ToLowerInvariant()
$__startTime = Get-Date
# ------------------------- HEADER GLOBALE -------------------------
$ResolvedInputPath = if ($InputPath) { Resolve-RepoPath $InputPath } else { 'N/A' }
$ResolvedOutputPath = if ($OutputPath) { Resolve-RepoPath $OutputPath } else { 'N/A' }
Write-Host "=== ImageMagick Toolkit ===" -ForegroundColor Cyan
Write-Host "Action : $Action" -ForegroundColor Gray
Write-Host "Input : $InputPath" -ForegroundColor Gray
Write-Host " resolved -> $ResolvedInputPath" -ForegroundColor DarkGray
Write-Host "Output : $OutputPath" -ForegroundColor Gray
Write-Host " resolved -> $ResolvedOutputPath" -ForegroundColor DarkGray
Write-Host "Magick : $Magick" -ForegroundColor Gray
Write-Host "Verbose: $Verbose" -ForegroundColor Gray
Write-Host "Prefix : $Prefix" -ForegroundColor Gray
Write-Host "Format : $Format" -ForegroundColor Gray
Write-Host "Colors : $Colors" -ForegroundColor Gray
Write-Host "NoDith : $NoDither" -ForegroundColor Gray
Write-Host "Started: $($__startTime.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor Gray
Write-Host "---------------------------" -ForegroundColor DarkGray
# ------------------------- DISPATCH -------------------------
switch ($actionLower)
{
'identify' {
$inPath = Invoke-InputPath $InputPath
if ($Verbose) { Write-Host "[VERBOSE] Running: $Magick identify $inPath" -ForegroundColor DarkGray }
& $Magick identify $inPath
}
'convert' {
if (-not $InputPath -or -not $OutputPath)
{
Fail 'Missing required options: --input, --output'
}
$inPath = Invoke-InputPath $InputPath
$outPath = Resolve-RepoPath $OutputPath
if ($Verbose) { Write-Host "[VERBOSE] Running: $Magick $inPath $outPath" -ForegroundColor DarkGray }
& $Magick $inPath $outPath
Write-Host "✅ Converted: $inPath → $outPath" -ForegroundColor Green
}
'resize' {
if (-not $InputPath -or -not $OutputPath -or -not $Width)
{
Fail 'Missing required options: --input, --output, --width'
}
$inPath = Invoke-InputPath $InputPath
$outPath = Resolve-RepoPath $OutputPath
$size = if ($Height)
{
"${Width}x${Height}"
}
else
{
"$Width"
}
if ($Verbose) { Write-Host "[VERBOSE] Running: magick (v7=$IsMagick7) resize $inPath -> $outPath ($size)" -ForegroundColor DarkGray }
if ($IsMagick7) {
& $Magick $inPath -resize $size $outPath
} else {
& $Magick convert $inPath -resize $size $outPath
}
if ($LASTEXITCODE -ne 0) { Fail "Resize failed (exit code $LASTEXITCODE)" }
Write-Host "✅ Resized $inPath → $outPath ($size)" -ForegroundColor Green
}
'optimize' {
$inPath = Invoke-InputPath $InputPath
$outPath = if ($OutputPath)
{
Resolve-RepoPath $OutputPath
}
else
{
Resolve-RepoPath $InputPath
}
if ($Verbose) { Write-Host "[VERBOSE] Running: magick (v7=$IsMagick7) optimize $inPath -> $outPath" -ForegroundColor DarkGray }
if ($IsMagick7) {
& $Magick $inPath -strip -interlace Plane -quality 85% $outPath
} else {
& $Magick convert $inPath -strip -interlace Plane -quality 85% $outPath
}
if ($LASTEXITCODE -ne 0) { Fail "Optimize failed (exit code $LASTEXITCODE)" }
Write-Host "✅ Optimized $inPath → $outPath" -ForegroundColor Green
}
'make-ico' {
if (-not $InputPath -or -not $OutputPath -or -not $Sizes)
{
Fail 'Missing required options: --input, --output, --sizes'
}
$inPath = Invoke-InputPath $InputPath
$outPath = Resolve-RepoPath $OutputPath
$sizesList = $Sizes -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^[0-9]+$' }
if (-not $sizesList)
{
Fail 'Invalid --sizes list (expected comma-separated numbers)'
}
# Build ICO preserving alpha (IM7 supports auto-resize)
if ($Verbose) { Write-Host "[VERBOSE] Source alpha detection..." -ForegroundColor DarkGray }
$channelInfo = (& $Magick identify -format "%[channels]" $inPath 2>$null)
$hasAlpha = $false; if ($channelInfo -and ($channelInfo -match 'a' -or $channelInfo -match 'alpha')) { $hasAlpha = $true }
if ($Verbose) { Write-Host "[VERBOSE] Channels: $channelInfo Alpha=$hasAlpha" -ForegroundColor DarkGray }
$joinedSizes = ($sizesList -join ',')
$quantArgs = @()
if ($Colors) {
if ($Colors -notmatch '^[0-9]+$') { Fail "--colors must be numeric" }
$quantArgs += '-colors'; $quantArgs += $Colors
if ($NoDither) { $quantArgs += '-dither'; $quantArgs += 'None' }
}
if ($IsMagick7) {
# One-shot pipeline
$cmdArgs = @($inPath, '-background','none')
if ($hasAlpha) { $cmdArgs += '-alpha'; $cmdArgs += 'set' } else { $cmdArgs += '-alpha'; $cmdArgs += 'off' }
$cmdArgs += '-define'; $cmdArgs += "icon:auto-resize=$joinedSizes"
if ($Colors) { $cmdArgs += $quantArgs }
$cmdArgs += '-depth'; $cmdArgs += '32'
$cmdArgs += $outPath
if ($Verbose) { Write-Host "[VERBOSE] Running IM7 ICO build: $Magick $($cmdArgs -join ' ')" -ForegroundColor DarkGray }
& $Magick @cmdArgs
if ($LASTEXITCODE -ne 0) { Fail "ICO build failed (exit $LASTEXITCODE)" }
} else {
# Legacy path: create temp PNG32 images then assemble
$tempFiles = @()
foreach ($s in $sizesList) {
$tmp = [System.IO.Path]::GetTempFileName() + ".png"
$resizeArgs = @($inPath,'-background','none')
if ($hasAlpha) { $resizeArgs += '-alpha'; $resizeArgs += 'set' }
$resizeArgs += '-resize'; $resizeArgs += "${s}x${s}"
$resizeArgs += "PNG32:$tmp"
if ($Verbose) { Write-Host "[VERBOSE] Gen size ${s}x${s}: $Magick $($resizeArgs -join ' ')" -ForegroundColor DarkGray }
& $Magick @resizeArgs
if ($LASTEXITCODE -ne 0) { Fail "Failed generating size ${s}x${s} (exit $LASTEXITCODE)" }
$tempFiles += $tmp
}
$assemble = @()
$assemble += $tempFiles
if ($Colors) { $assemble += $quantArgs }
$assemble += $outPath
if ($Verbose) { Write-Host "[VERBOSE] Assemble legacy ICO: $Magick convert $($assemble -join ' ')" -ForegroundColor DarkGray }
& $Magick convert @assemble
if ($LASTEXITCODE -ne 0) { Fail "ICO assembly failed (exit $LASTEXITCODE)" }
foreach ($tmp in $tempFiles) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
}
Write-Host "✅ ICO created: $outPath (sizes: $joinedSizes alpha=$hasAlpha colors=$Colors)" -ForegroundColor Green
}
'extract' {
if (-not $InputPath) { Fail 'Missing --input for extract action' }
$inPath = Invoke-InputPath $InputPath
# Output directory default now fixed to 'layers/' under input folder if not provided
$outDir = if ($OutputPath) { Resolve-RepoPath $OutputPath } else { Join-Path (Split-Path -Parent $inPath) 'layers' }
if (-not (Test-Path -LiteralPath $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
if ($Verbose) { Write-Host "[VERBOSE] Extract output directory: $outDir" -ForegroundColor DarkGray }
$fmt = $Format.ToLower()
if ($fmt -eq 'jpeg') { $fmt = 'jpg' }
if ($fmt -notin @('png','jpg','webp')) { Fail "Unsupported --format '$Format' (allowed: png, jpg, webp)" }
# Frame count
$frameCount = (& $Magick identify -format "%n" $inPath)
if ($Verbose) { Write-Host "[VERBOSE] Frame count detected: $frameCount" -ForegroundColor DarkGray }
if (-not $frameCount -or [int]$frameCount -le 0) { Fail "No frames detected in icon: $inPath" }
$exported = @()
for ($i = 0; $i -lt [int]$frameCount; $i++) {
$outFile = Join-Path $outDir ("${Prefix}_{0}.${fmt}" -f $i)
if ($Verbose) { Write-Host "[VERBOSE] Extract layer $i (v7=$IsMagick7) -> $outFile" -ForegroundColor DarkGray }
if ($IsMagick7) {
& $Magick $inPath[$i] $outFile
} else {
& $Magick convert $inPath[$i] $outFile
}
if ($LASTEXITCODE -ne 0) { Fail "Failed to extract layer index $i (exit $LASTEXITCODE)" }
$exported += $outFile
}
Write-Host "✅ Extracted ${frameCount} layer(s) to: $outDir (format: $fmt, prefix: $Prefix)" -ForegroundColor Green
if ($Verbose) {
Write-Host "[VERBOSE] Files:" -ForegroundColor DarkGray
$exported | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
}
}
default {
Fail "Unknown action $Action"
}
}
$__elapsed = (Get-Date) - $__startTime
if ($Verbose) { Write-Host "[VERBOSE] Elapsed: $([int]$__elapsed.TotalMilliseconds) ms" -ForegroundColor DarkGray }
Write-Host ""; Write-Host "🎉 Operation completed successfully." -ForegroundColor Green
exit 0