Add mixed-orientation layout for -Clip mode with landscape video at native size and portrait video scaled to match height

This commit is contained in:
2026-06-03 15:26:45 -04:00
parent edf9f6b874
commit a687bb5348
2 changed files with 91 additions and 22 deletions
+87 -21
View File
@@ -58,8 +58,19 @@
the top/bottom black letterbox padding. Only applies when exactly 2 source videos are the top/bottom black letterbox padding. Only applies when exactly 2 source videos are
provided; ignored otherwise. provided; ignored otherwise.
Example: two 1280x720 (16:9) sources rendered at 1K HD become 1920x540 instead of Behavior depends on the orientations of the two sources:
1920x1080. The same pair rendered with -In4K becomes 3840x1080 instead of 3840x2160.
- Both same orientation (both landscape or both portrait): the output height is set
to the larger of the two sources' scaled heights inside their half-width cells.
Example: two 1280x720 sources -> 1920x540 (1K) or 3840x1080 (4K).
- Mixed orientations (one landscape, one portrait), and the landscape source is
smaller than the target resolution: the landscape video occupies its native pixel
width at 1K HD (or 2x in 4K), and the portrait video is scaled to match the
landscape's height and centered in the remaining width with black pillar boxes.
Example: 1280x720 landscape + 720x1280 portrait at 1K HD -> 1920x720 (landscape
occupies 1280x720 on its S1/S2 side, portrait fits in the remaining 640x720).
At 4K the same pair becomes 3840x1440 (landscape 2560x1440, portrait cell 1280x1440).
.EXAMPLE .EXAMPLE
.\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison.mp4 .\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison.mp4
@@ -186,6 +197,8 @@ if ($isPortrait) {
} }
# --- Clip mode: shrink output height to the actual scaled height of the sources --- # --- Clip mode: shrink output height to the actual scaled height of the sources ---
$clipMixedLandscape = $false
$clipMixedFilterComplex = $null
if ($Clip) { if ($Clip) {
Write-Host "Probing S2 dimensions for -Clip..." -ForegroundColor Cyan Write-Host "Probing S2 dimensions for -Clip..." -ForegroundColor Cyan
$probeOutput2 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S2 2>&1 $probeOutput2 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S2 2>&1
@@ -197,34 +210,87 @@ if ($Clip) {
$w2 = [int]$dim2[0] $w2 = [int]$dim2[0]
$h2 = [int]$dim2[1] $h2 = [int]$dim2[1]
# Per-source cell width matches the 2-video branches below. $s1IsLandscape = $width -gt $height
if ($isPortrait) { $s2IsLandscape = $w2 -gt $h2
$clipCellWidth = [int]($outputWidth * 0.6333 / 2) $isMixedOrientation = ($s1IsLandscape -xor $s2IsLandscape)
} else {
$clipCellWidth = [int]($outputWidth / 2) if ($isMixedOrientation) {
# One source is landscape, the other is portrait. Try the mixed-orientation
# layout: the landscape video keeps its native pixel size at 1K HD (or 2x in 4K),
# and the portrait video is scaled to match the landscape's height and centered
# in the remaining width with black pillar-box bars.
$scaleFactor = $outputWidth / 1920.0 # 1.0 at 1K HD, 2.0 at 4K UHD
if ($s1IsLandscape) {
$landscapeW = $width; $landscapeH = $height
} else {
$landscapeW = $w2; $landscapeH = $h2
}
$landscapeOutW = [int][Math]::Floor($landscapeW * $scaleFactor)
$landscapeOutH = [int][Math]::Floor($landscapeH * $scaleFactor)
if ($landscapeOutW % 2) { $landscapeOutW++ }
if ($landscapeOutH % 2) { $landscapeOutH++ }
if ($landscapeOutW -lt $outputWidth -and $landscapeOutH -le $outputHeight) {
$portraitCellW = $outputWidth - $landscapeOutW
$outputHeight = $landscapeOutH
Write-Host ("Clip mode (mixed orientation): landscape {0}x{1} + portrait {2}x{1} -> {3}x{1}" -f $landscapeOutW, $landscapeOutH, $portraitCellW, $outputWidth) -ForegroundColor Yellow
# Preserve S1=left, S2=right ordering. Each input is scaled into its cell
# with aspect-ratio preserved, then padded with black to exactly fill the cell.
if ($s1IsLandscape) {
$leftW = $landscapeOutW; $rightW = $portraitCellW
} else {
$leftW = $portraitCellW; $rightW = $landscapeOutW
}
$clipMixedFilterComplex = "[0:v]scale=${leftW}:${landscapeOutH}:force_original_aspect_ratio=decrease,pad=${leftW}:${landscapeOutH}:(ow-iw)/2:(oh-ih)/2:black[v0];" +
"[1:v]scale=${rightW}:${landscapeOutH}:force_original_aspect_ratio=decrease,pad=${rightW}:${landscapeOutH}:(ow-iw)/2:(oh-ih)/2:black[v1];" +
"[v0][v1]hstack=inputs=2[outv]"
$clipMixedLandscape = $true
} else {
Write-Host "Clip mode: mixed orientations detected, but the landscape source already fills the target width; using standard clip layout." -ForegroundColor Yellow
}
} }
# Height each source would naturally scale to inside its cell when fitted by width if (-not $clipMixedLandscape) {
# (force_original_aspect_ratio=decrease). Take the larger of the two so neither # Standard -Clip behaviour: both sources share an orientation (or mixed fell back).
# source loses content; cap at the original output height. # Compute the largest natural scaled height across both sources and reduce the
$h1Scaled = [int][Math]::Floor($clipCellWidth * $height / $width) # output height to that value.
$h2Scaled = [int][Math]::Floor($clipCellWidth * $h2 / $w2)
$clippedHeight = [Math]::Max($h1Scaled, $h2Scaled)
if ($clippedHeight % 2) { $clippedHeight++ } # libx264/yuv420p requires even dimensions
if ($clippedHeight -gt $outputHeight) { $clippedHeight = $outputHeight }
if ($clippedHeight -lt $outputHeight) { # Per-source cell width matches the 2-video branches below.
Write-Host ("Clip mode: output height reduced from {0} to {1} ({2}x{1})" -f $outputHeight, $clippedHeight, $outputWidth) -ForegroundColor Yellow if ($isPortrait) {
$outputHeight = $clippedHeight $clipCellWidth = [int]($outputWidth * 0.6333 / 2)
} else { } else {
Write-Host "Clip mode: sources already fill the full output height; no clipping applied." -ForegroundColor Yellow $clipCellWidth = [int]($outputWidth / 2)
}
# Height each source would naturally scale to inside its cell when fitted by width
# (force_original_aspect_ratio=decrease). Take the larger of the two so neither
# source loses content; cap at the original output height.
$h1Scaled = [int][Math]::Floor($clipCellWidth * $height / $width)
$h2Scaled = [int][Math]::Floor($clipCellWidth * $h2 / $w2)
$clippedHeight = [Math]::Max($h1Scaled, $h2Scaled)
if ($clippedHeight % 2) { $clippedHeight++ } # libx264/yuv420p requires even dimensions
if ($clippedHeight -gt $outputHeight) { $clippedHeight = $outputHeight }
if ($clippedHeight -lt $outputHeight) {
Write-Host ("Clip mode: output height reduced from {0} to {1} ({2}x{1})" -f $outputHeight, $clippedHeight, $outputWidth) -ForegroundColor Yellow
$outputHeight = $clippedHeight
} else {
Write-Host "Clip mode: sources already fill the full output height; no clipping applied." -ForegroundColor Yellow
}
} }
} }
$filterComplex = "" $filterComplex = ""
$inputArgs = @("-i", $S1, "-i", $S2) $inputArgs = @("-i", $S1, "-i", $S2)
if ($isPortrait) { if ($clipMixedLandscape) {
# Mixed-orientation -Clip layout was prepared above; use it as-is.
$filterComplex = $clipMixedFilterComplex
} elseif ($isPortrait) {
# Portrait mode scaling - calculate cell dimensions based on output resolution # Portrait mode scaling - calculate cell dimensions based on output resolution
$cellWidth2 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K $cellWidth2 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K
$cellWidth3 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K $cellWidth3 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K
+4 -1
View File
@@ -161,7 +161,10 @@ try {
$valueDisplay = ('{0} frames ({1}s, {2} fps)' -f $frames, $durationSeconds, $fps) $valueDisplay = ('{0} frames ({1}s, {2} fps)' -f $frames, $durationSeconds, $fps)
} }
elseif ($fieldValue -is [array]) { elseif ($fieldValue -is [array]) {
$valueDisplay = ($fieldValue | ConvertTo-Json -Compress -Depth 10) # Pretty-print array values as JSON, prefixed with a newline so the
# multi-line output starts cleanly under the field label.
$jsonArray = $fieldValue | ConvertTo-Json -Depth 10
$valueDisplay = [Environment]::NewLine + $jsonArray
} }
else { else {
$valueDisplay = $fieldValue $valueDisplay = $fieldValue