Skip to content

Instantly share code, notes, and snippets.

@arenagroove
Last active May 13, 2025 06:55
Show Gist options
  • Select an option

  • Save arenagroove/6b6acba5527c33381bd1528e7ae7d6f2 to your computer and use it in GitHub Desktop.

Select an option

Save arenagroove/6b6acba5527c33381bd1528e7ae7d6f2 to your computer and use it in GitHub Desktop.
PowerShell + FFmpeg script to generate WordPress-like thumbnails with enforced aspect ratio, cropping, format conversion, optional metadata, and per-dimension output folders. Supports both width-driven and height-driven resizing. Optimized for speed and automation.

Generate-WordPress-Like-Thumbnails.ps1

A PowerShell + FFmpeg script to generate WordPress-style thumbnails with enforced aspect ratio, cropping, format conversion, optional metadata, and per-dimension output folders.
Now supports height-based resizing as an alternative to width-based scaling.
Optimized for automation, performance, and precision.

✅ Features

  • Multi-size output (e.g. 820x820, 435x245, 300x300)
  • Resizing based on width (default) or height
  • Optional fixed aspect ratio (e.g. 1:1, 16:9)
  • Format support: webp, jpg, png
  • Optional per-width subfolders (e.g. ./thumbnails/435/)
  • Optional filename suffixes (e.g. -md, -square)
  • Optional metadata embedding (Software=WPThumbnailGenerator)
  • Overwrite protection and original file safeguard
  • Fast execution using PowerShell call operator (& ffmpeg)
  • FFmpeg/FFprobe required – no external modules

🧰 Requirements

  • PowerShell 5.1+ (recommended: PowerShell 7+)
  • FFmpeg and FFprobe available in your system PATH

🚀 Example Usage

# Width-based resize (default)
.\Generate-WordPress-Like-Thumbnails.ps1 `
  -inputFolder ".\images" `
  -outputFolder ".\thumbnails" `
  -targetWidths @(820, 435, 300) `
  -forceAspectRatio "1:1" `
  -outputFormats @("webp", "jpg") `
  -nameSuffix "square" `
  -overwrite $true `
  -addMetadata $true `
  -usePerWidthSubfolder $true

# Height-based resize (alternative)
.\Generate-WordPress-Like-Thumbnails.ps1 `
  -inputFolder ".\images" `
  -outputFolder ".\thumbnails" `
  -targetHeights @(600, 400) `
  -outputFormats @("webp") `
  -forceAspectRatio "4:3"

📝 Parameters

Name Type Description
inputFolder string Source directory (default: .)
outputFolder string Destination directory (default: .)
targetWidths string[] List of target widths in pixels (mutually exclusive with heights)
targetHeights string[] List of target heights in pixels (mutually exclusive with widths)
outputFormats string[] Output formats (e.g. @("webp", "jpg"))
forceAspectRatio string Aspect ratio like "1:1", "16:9" or $null
nameSuffix string Optional suffix before dimensions (e.g. -md, -square)
overwrite bool Overwrite existing thumbnails (default: true)
addMetadata bool Embed metadata in output (default: false)
usePerWidthSubfolder bool Output into outputFolder/width/ subfolders (default: true)
validExtensions string[] Allowed input extensions (default: .jpg, .jpeg, .png, .webp)

📂 Output Structure

thumbnails/
├── 820/
│   └── image-square-820x820.webp
├── 435/
│   └── image-square-435x435.webp

📎 Notes

  • targetWidths and targetHeights are mutually exclusive.
  • Aspect ratio logic is respected for both width and height modes.
  • Original files are never overwritten.
param (
[string[]]$targetWidths = @(), # Provide either this...
[string[]]$targetHeights = @(), # ...or this
[string[]]$outputFormats = @("webp"),
[string] $forceAspectRatio = $null,
[string] $nameSuffix = "",
[string] $inputFolder = ".",
[string] $outputFolder = ".",
[bool] $overwrite = $true,
[bool] $addMetadata = $false,
[bool] $usePerWidthSubfolder = $true,
[string[]]$validExtensions = @(".jpg", ".jpeg", ".png", ".webp")
)
# Validate mode
if ($targetWidths.Count -gt 0 -and $targetHeights.Count -gt 0) {
throw "❌ Provide either -targetWidths or -targetHeights, not both."
}
$resizeMode = if ($targetHeights.Count -gt 0) { "height" } else { "width" }
$sizes = if ($resizeMode -eq "height") { $targetHeights } else { $targetWidths }
# Format-specific quality
$jpgQuality = 2
$pngCompression = 2
$webpQuality = 98
function LogInfo($msg) {
Write-Host "$(Get-Date -Format 'HH:mm:ss') | $msg"
}
if ($forceAspectRatio -and ($forceAspectRatio -notmatch "^\d+:\d+$")) {
throw "❌ Invalid forceAspectRatio format. Use 'W:H' (e.g. 16:9, 1:1)."
}
Get-ChildItem -Path $inputFolder -File | Where-Object {
$validExtensions -contains $_.Extension.ToLower()
} | ForEach-Object {
$originalPath = $_.FullName
$filename = $_.Name
$cleanName = $_.BaseName -replace "-\d+x\d+$", ""
try {
$width = ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "`"$originalPath`""
$height = ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "`"$originalPath`""
$width = [int]$width
$height = [int]$height
if ($width -eq 0 -or $height -eq 0) {
LogInfo "⚠️ Skipping $filename (invalid dimensions)"
return
}
} catch {
LogInfo "❌ Failed to probe $filename. Skipping."
return
}
$OAR = $width / $height
foreach ($size in $sizes) {
$targetWidth = 0
$targetHeight = 0
if ($forceAspectRatio) {
$parts = $forceAspectRatio -split ":"
$faWidth = [int]$parts[0]
$faHeight = [int]$parts[1]
if ($resizeMode -eq "height") {
$targetHeight = [int]$size
$targetWidth = [math]::Round($targetHeight * ($faWidth / $faHeight))
} else {
$targetWidth = [int]$size
$targetHeight = [math]::Round($targetWidth * ($faHeight / $faWidth))
}
$TAR = $faWidth / $faHeight
} else {
if ($resizeMode -eq "height") {
$targetHeight = [int]$size
$targetWidth = [math]::Round($targetHeight * $OAR)
} else {
$targetWidth = [int]$size
$targetHeight = [math]::Round($targetWidth / $OAR)
}
$TAR = $targetWidth / $targetHeight
}
if ([math]::Round($OAR, 3) -eq [math]::Round($TAR, 3)) {
$vfFilter = "scale=${targetWidth}:${targetHeight},setsar=1"
}
elseif ($OAR -gt $TAR) {
$vfFilter = "scale=-2:${targetHeight},crop=${targetWidth}:${targetHeight}:(iw-${targetWidth})/2:0,setsar=1"
}
else {
$vfFilter = "scale=${targetWidth}:-2,crop=${targetWidth}:${targetHeight}:0:(ih-${targetHeight})/2,setsar=1"
}
foreach ($fmt in $outputFormats) {
$outputName = if ($nameSuffix -ne "") {
"${cleanName}-${nameSuffix}-${targetWidth}x${targetHeight}.${fmt}"
} else {
"${cleanName}-${targetWidth}x${targetHeight}.${fmt}"
}
$targetFolder = if ($usePerWidthSubfolder) {
$sub = Join-Path $outputFolder "$targetWidth"
if (-not (Test-Path $sub)) {
New-Item -Path $sub -ItemType Directory -Force | Out-Null
}
$sub
} else {
$outputFolder
}
$outputPath = Join-Path $targetFolder $outputName
if (-not $overwrite -and (Test-Path $outputPath)) {
LogInfo "⏭️ Skipping existing: $outputName"
continue
}
if ($outputPath -eq $originalPath) {
LogInfo "⚠️ Skipping to avoid overwriting original file: $outputName"
continue
}
$args = @("-i", $originalPath, "-vf", $vfFilter)
switch ($fmt.ToLower()) {
"jpg" { $args += @("-q:v", "$jpgQuality") }
"png" { $args += @("-compression_level", "$pngCompression") }
"webp" { $args += @("-q:v", "$webpQuality") }
default {
LogInfo "❌ Unsupported format: $fmt — skipping."
continue
}
}
if ($addMetadata) {
$args += @("-metadata", "Software=WPThumbnailGenerator")
}
$args += @("-y", $outputPath)
try {
& ffmpeg @args
LogInfo "✅ $filename → $outputName"
} catch {
LogInfo "❌ Error generating `${outputName}`: $_"
}
}
}
}
LogInfo "`n✅ Thumbnail generation complete."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment