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

This commit is contained in:
guru committed 2026-10-14 09:53:09 -04:00
1 parent aaa293734e
commit 70b4b83be0
1 file changed
+69 -13
+69 -13
View File
@@ -14,11 +14,11 @@
from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified). from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified).
- -Sequence and -Metadata are not allowed in this mode. - -Sequence and -Metadata are not allowed in this mode.
- Defaults: 24 FPS, CRF 8, no audio (unless -Audio is provided). - 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. 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. - -NoAudio is not allowed in this mode.
- Defaults: 24 FPS, CRF 8, source image dimensions. - 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. - -StartFrame, -EndFrame, -Frames, -Sequence, -Metadata and -Reverse are ignored.
.PARAMETER Source .PARAMETER Source
Path to the input video file, or a directory containing PNG files with sequence numbers (e.g. frame_0000.png). 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) - 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) - 480p: 832x448 (16:9), 448x832 (9:16), 896x512 (4:3), 512x896 (3:4)
Forces a video re-encode (same as -Size). 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 .DESCRIPTION
This script uses ffmpeg to clone (copy) a video file with optional frame range and audio exclusion. 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. When Sequence is provided, it extracts a specific frame to PNG and then clones the video accordingly.
@@ -123,7 +133,10 @@ param(
[switch]$Reverse, [switch]$Reverse,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[switch]$LTX [switch]$LTX,
[Parameter(Mandatory = $false)]
[switch]$Crop
) )
Set-StrictMode -Version Latest 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 $PSNativeCommandUseErrorActionPreference = $false
# Image-to-video mode: when $Source is a supported image file and $Audio is provided. # 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 ($imgOutWidth % 2) { $imgOutWidth++ }
if ($imgOutHeight % 2) { $imgOutHeight++ } if ($imgOutHeight % 2) { $imgOutHeight++ }
if ($imgOutWidth -gt 0) {
$imgSizeFilter = Get-SizeFilter $imgSrcWidth $imgSrcHeight $imgOutWidth $imgOutHeight
$imgOutWidth = $imgSizeFilter.Width
$imgOutHeight = $imgSizeFilter.Height
}
$imgFfmpegArgs = @( $imgFfmpegArgs = @(
'-y' '-y'
'-loop', '1' '-loop', '1'
@@ -365,7 +409,7 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) {
) )
if ($imgOutWidth -gt 0) { if ($imgOutWidth -gt 0) {
$imgFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $imgOutWidth, $imgOutHeight)) $imgFfmpegArgs += @('-vf', $imgSizeFilter.Filter)
} }
$imgFfmpegArgs += $targetVideoEncodeArgs $imgFfmpegArgs += $targetVideoEncodeArgs
@@ -391,9 +435,9 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) {
Write-Host (' FPS: {0}' -f $imgFPS) Write-Host (' FPS: {0}' -f $imgFPS)
if ($imgOutWidth -gt 0) { if ($imgOutWidth -gt 0) {
if ($LTX) { 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 { } 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) Write-Host (' CRF: {0}' -f $imgCRF)
@@ -542,6 +586,12 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
if ($seqOutWidth % 2) { $seqOutWidth++ } if ($seqOutWidth % 2) { $seqOutWidth++ }
if ($seqOutHeight % 2) { $seqOutHeight++ } 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() $concatTempFile = [System.IO.Path]::GetTempFileName()
try { try {
$frameDuration = (1.0 / $seqFPS).ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture) $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)) $seqFfmpegArgs += @('-r', $seqFPS.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture))
if ($seqOutWidth -gt 0) { if ($seqOutWidth -gt 0) {
$seqFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $seqOutWidth, $seqOutHeight)) $seqFfmpegArgs += @('-vf', $seqSizeFilter.Filter)
} }
$seqFfmpegArgs += $targetVideoEncodeArgs $seqFfmpegArgs += $targetVideoEncodeArgs
@@ -593,9 +643,9 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
Write-Host (' FPS: {0}' -f $seqFPS) Write-Host (' FPS: {0}' -f $seqFPS)
if ($seqOutWidth -gt 0) { if ($seqOutWidth -gt 0) {
if ($LTX) { 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 { } 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) Write-Host (' CRF: {0}' -f $seqCRF)
@@ -758,6 +808,12 @@ if ($LTX) {
$scaleHeight = $best.H $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 $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 $ffprobeExit = $LASTEXITCODE
@@ -882,9 +938,9 @@ if ($AudioPath -and $externalAudioCodec) {
Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount) Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount)
if ($scaleWidth -gt 0) { if ($scaleWidth -gt 0) {
if ($LTX) { 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 { } 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) { 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 = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame)
$filterParts += 'setpts=PTS-STARTPTS' $filterParts += 'setpts=PTS-STARTPTS'
if ($scaleWidth -gt 0) { if ($scaleWidth -gt 0) {
$filterParts += ('scale={0}:{1}' -f $scaleWidth, $scaleHeight) $filterParts += $sizeFilter.Filter
} }
if ($FPS -gt 0) { if ($FPS -gt 0) {
$filterParts += ('fps={0}' -f $FPS) $filterParts += ('fps={0}' -f $FPS)
@@ -937,7 +993,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
} elseif ($needsReencode) { } elseif ($needsReencode) {
$filterParts = @() $filterParts = @()
if ($scaleWidth -gt 0) { if ($scaleWidth -gt 0) {
$filterParts += ('scale={0}:{1}' -f $scaleWidth, $scaleHeight) $filterParts += $sizeFilter.Filter
} }
if ($FPS -gt 0) { if ($FPS -gt 0) {
$filterParts += ('fps={0}' -f $FPS) $filterParts += ('fps={0}' -f $FPS)