From 70b4b83be0fd77bf6d0c38ff13c7ca4c5d9780fb Mon Sep 17 00:00:00 2001 From: Claude Toupin Date: Wed, 14 Oct 2026 09:53:09 -0400 Subject: [PATCH] Add -Crop parameter to Clone-Video.ps1 for center-cropping instead of resizing when combined with -Size, implementing Get-SizeFilter function that crops only dimensions larger than requested size with even-number rounding, applying crop filter across image-to-video, PNG sequence, and standard video modes, and updating documentation and output messages to indicate cropped dimensions --- Clone-Video.ps1 | 82 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/Clone-Video.ps1 b/Clone-Video.ps1 index d9b5d81..922650f 100644 --- a/Clone-Video.ps1 +++ b/Clone-Video.ps1 @@ -14,11 +14,11 @@ from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified). - -Sequence and -Metadata are not allowed in this mode. - Defaults: 24 FPS, CRF 8, no audio (unless -Audio is provided). - - -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse and -LTX are all honoured. + - -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse, -LTX and -Crop are all honoured. When Source is a supported image file (.png, .jpg, .jpeg, .webp, .bmp, .tiff, .tif, .gif) and Audio is provided, the script builds a video of the static image for the duration of the audio file. - -NoAudio is not allowed in this mode. - Defaults: 24 FPS, CRF 8, source image dimensions. - - -Size, -FPS, -CRF and -LTX are honoured. + - -Size, -FPS, -CRF, -LTX and -Crop are honoured. - -StartFrame, -EndFrame, -Frames, -Sequence, -Metadata and -Reverse are ignored. .PARAMETER Source Path to the input video file, or a directory containing PNG files with sequence numbers (e.g. frame_0000.png). @@ -74,6 +74,16 @@ - 720p: 1280x704 (16:9), 704x1280 (9:16), 1088x576 (21:9), 576x1088 (9:21) - 480p: 832x448 (16:9), 448x832 (9:16), 896x512 (4:3), 512x896 (3:4) Forces a video re-encode (same as -Size). +.PARAMETER Crop + When present, the dimensions selected by -Size (including -LTX buckets) are obtained by center-cropping + the source instead of resizing it, so the original pixel aspect ratio is kept (no stretching or scaling). + Each dimension is cropped only when the source is bigger than the requested size; a dimension where the + source is equal or smaller is left untouched (no upscaling or padding). Cropped dimensions are rounded + down to an even number. Examples for a 1080x1080 source: + - -Size 960:1080 -Crop: cropped horizontally to 960x1080. + - -Size 720:720 -Crop: cropped horizontally and vertically to 720x720. + Requires -Size. Without -Crop, the source is resized (default behaviour). + Forces a video re-encode (same as -Size). .DESCRIPTION This script uses ffmpeg to clone (copy) a video file with optional frame range and audio exclusion. When Sequence is provided, it extracts a specific frame to PNG and then clones the video accordingly. @@ -123,7 +133,10 @@ param( [switch]$Reverse, [Parameter(Mandatory = $false)] - [switch]$LTX + [switch]$LTX, + + [Parameter(Mandatory = $false)] + [switch]$Crop ) Set-StrictMode -Version Latest @@ -271,6 +284,31 @@ if (-not [string]::IsNullOrWhiteSpace($Size)) { } } +if ($Crop -and [string]::IsNullOrWhiteSpace($Size)) { + Write-Error '-Crop requires -Size' + exit 1 +} + +# Builds the ffmpeg size filter for the requested output dimensions. +# Without -Crop the source is resized (scale). With -Crop the source is center-cropped, +# only on the dimensions where it is bigger than the requested size. +function Get-SizeFilter { + param([int]$SrcWidth, [int]$SrcHeight, [int]$OutWidth, [int]$OutHeight) + if (-not $Crop) { + return @{ Filter = ('scale={0}:{1}' -f $OutWidth, $OutHeight); Width = $OutWidth; Height = $OutHeight } + } + if ($SrcWidth -le 0 -or $SrcHeight -le 0) { + Write-Error 'Could not determine source dimensions; -Crop cannot compute the crop area.' + exit 1 + } + $cropWidth = [Math]::Min($OutWidth, $SrcWidth) + $cropHeight = [Math]::Min($OutHeight, $SrcHeight) + $cropWidth -= $cropWidth % 2 + $cropHeight -= $cropHeight % 2 + return @{ Filter = ('crop={0}:{1}' -f $cropWidth, $cropHeight); Width = $cropWidth; Height = $cropHeight } +} +$cropNote = if ($Crop) { ' (cropped)' } else { '' } + $PSNativeCommandUseErrorActionPreference = $false # Image-to-video mode: when $Source is a supported image file and $Audio is provided. @@ -356,6 +394,12 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) { if ($imgOutWidth % 2) { $imgOutWidth++ } if ($imgOutHeight % 2) { $imgOutHeight++ } + if ($imgOutWidth -gt 0) { + $imgSizeFilter = Get-SizeFilter $imgSrcWidth $imgSrcHeight $imgOutWidth $imgOutHeight + $imgOutWidth = $imgSizeFilter.Width + $imgOutHeight = $imgSizeFilter.Height + } + $imgFfmpegArgs = @( '-y' '-loop', '1' @@ -365,7 +409,7 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) { ) if ($imgOutWidth -gt 0) { - $imgFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $imgOutWidth, $imgOutHeight)) + $imgFfmpegArgs += @('-vf', $imgSizeFilter.Filter) } $imgFfmpegArgs += $targetVideoEncodeArgs @@ -391,9 +435,9 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) { Write-Host (' FPS: {0}' -f $imgFPS) if ($imgOutWidth -gt 0) { if ($LTX) { - Write-Host (' Size: {0}x{1} (LTX-2)' -f $imgOutWidth, $imgOutHeight) + Write-Host (' Size: {0}x{1} (LTX-2){2}' -f $imgOutWidth, $imgOutHeight, $cropNote) } else { - Write-Host (' Size: {0}x{1}' -f $imgOutWidth, $imgOutHeight) + Write-Host (' Size: {0}x{1}{2}' -f $imgOutWidth, $imgOutHeight, $cropNote) } } Write-Host (' CRF: {0}' -f $imgCRF) @@ -542,6 +586,12 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) { if ($seqOutWidth % 2) { $seqOutWidth++ } if ($seqOutHeight % 2) { $seqOutHeight++ } + if ($seqOutWidth -gt 0) { + $seqSizeFilter = Get-SizeFilter $seqSrcWidth $seqSrcHeight $seqOutWidth $seqOutHeight + $seqOutWidth = $seqSizeFilter.Width + $seqOutHeight = $seqSizeFilter.Height + } + $concatTempFile = [System.IO.Path]::GetTempFileName() try { $frameDuration = (1.0 / $seqFPS).ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture) @@ -568,7 +618,7 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) { $seqFfmpegArgs += @('-r', $seqFPS.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)) if ($seqOutWidth -gt 0) { - $seqFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $seqOutWidth, $seqOutHeight)) + $seqFfmpegArgs += @('-vf', $seqSizeFilter.Filter) } $seqFfmpegArgs += $targetVideoEncodeArgs @@ -593,9 +643,9 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) { Write-Host (' FPS: {0}' -f $seqFPS) if ($seqOutWidth -gt 0) { if ($LTX) { - Write-Host (' Size: {0}x{1} (LTX-2)' -f $seqOutWidth, $seqOutHeight) + Write-Host (' Size: {0}x{1} (LTX-2){2}' -f $seqOutWidth, $seqOutHeight, $cropNote) } else { - Write-Host (' Size: {0}x{1}' -f $seqOutWidth, $seqOutHeight) + Write-Host (' Size: {0}x{1}{2}' -f $seqOutWidth, $seqOutHeight, $cropNote) } } Write-Host (' CRF: {0}' -f $seqCRF) @@ -758,6 +808,12 @@ if ($LTX) { $scaleHeight = $best.H } +if ($scaleWidth -gt 0) { + $sizeFilter = Get-SizeFilter $sourceWidth $sourceHeight $scaleWidth $scaleHeight + $scaleWidth = $sizeFilter.Width + $scaleHeight = $sizeFilter.Height +} + $ffprobeBitrateOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1 $ffprobeExit = $LASTEXITCODE @@ -882,9 +938,9 @@ if ($AudioPath -and $externalAudioCodec) { Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount) if ($scaleWidth -gt 0) { if ($LTX) { - Write-Host (' Size: {0}x{1} (LTX-2)' -f $scaleWidth, $scaleHeight) + Write-Host (' Size: {0}x{1} (LTX-2){2}' -f $scaleWidth, $scaleHeight, $cropNote) } else { - Write-Host (' Size: {0}x{1}' -f $scaleWidth, $scaleHeight) + Write-Host (' Size: {0}x{1}{2}' -f $scaleWidth, $scaleHeight, $cropNote) } } if ($FPS -gt 0) { @@ -910,7 +966,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) { $filterParts = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame) $filterParts += 'setpts=PTS-STARTPTS' if ($scaleWidth -gt 0) { - $filterParts += ('scale={0}:{1}' -f $scaleWidth, $scaleHeight) + $filterParts += $sizeFilter.Filter } if ($FPS -gt 0) { $filterParts += ('fps={0}' -f $FPS) @@ -937,7 +993,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) { } elseif ($needsReencode) { $filterParts = @() if ($scaleWidth -gt 0) { - $filterParts += ('scale={0}:{1}' -f $scaleWidth, $scaleHeight) + $filterParts += $sizeFilter.Filter } if ($FPS -gt 0) { $filterParts += ('fps={0}' -f $FPS)