public/转GIF.ps1
|
function 转GIF { <# .SYNOPSIS 将视频的第一个视频轨转换为 GIF 动图。 .DESCRIPTION 使用 ffmpeg 的 palettegen/paletteuse 两遍调色板流程,把输入文件的第一个视频轨转成高质量 GIF: 第一遍分析全片生成优化调色板(stats_mode=diff),第二遍用该调色板逐帧量化输出, 画面色彩和过渡明显优于一遍直出的 GIF。 尺寸控制三选一:可以指定 -宽度 和/或 -高度(只指定其一时另一边按原视频比例自动计算; 同时指定时按给定宽高直接缩放,允许宽高比不同于原视频),或者指定 -缩放率(按原视频比例 整体缩放,1 表示保持原分辨率,小于 1 缩小、大于 1 放大)。两种尺寸方式互斥: 缩放率不能与宽度/高度同时使用。未指定任何尺寸参数时保持原始分辨率。 未指定输出路径时,在输入文件同目录生成“原文件名.gif”。输出文件已存在时会被覆盖。 .PARAMETER 输入文件 要转换的视频文件路径,必须存在且至少包含一个视频轨。 .PARAMETER 输出文件 输出 GIF 路径。省略时为输入文件同目录下的“原文件名.gif”。 .PARAMETER 宽度 目标 GIF 宽度(像素)。可与 -高度 搭配使用;只指定宽度时,高度按原视频比例自动计算。 与 -缩放率 互斥。 .PARAMETER 高度 目标 GIF 高度(像素)。可与 -宽度 搭配使用;只指定高度时,宽度按原视频比例自动计算。 与 -缩放率 互斥。 .PARAMETER 缩放率 按原视频宽高比整体缩放的倍率,任意正数,默认 1(不缩放);如 0.5 表示缩小为一半、 2 表示放大为两倍。与 -宽度/-高度 互斥。 .PARAMETER 帧率 GIF 帧率(每秒帧数),浮点数。0 表示保持原始帧率(默认);调低可显著减小体积,常用 10~15; 指定高于原视频的帧率时会自动插帧补顺。分数写法请用括号表达式传入,如 -帧率 (23.57/5.8), PowerShell 会先求值再传入。 .EXAMPLE 转GIF -输入文件 'D:\头像.mp4' 按原始分辨率和帧率转换为 D:\头像.gif。 .EXAMPLE 转GIF -输入文件 'D:\头像.mp4' -宽度 320 -帧率 15 宽度固定 320(高度按比例)并以 15fps 输出,是控制体积的常见用法。 .EXAMPLE 转GIF -输入文件 'D:\头像.mp4' -宽度 300 -高度 300 缩放到 300x300;宽高比与原视频不同时画面会按目标尺寸拉伸。 .EXAMPLE 转GIF -输入文件 'D:\片段.mkv' -缩放率 0.5 -输出文件 'D:\表情.gif' 按原视频比例缩小到一半,并指定输出路径。 .EXAMPLE 转GIF -输入文件 'D:\片段.mkv' -缩放率 2 按原视频比例放大为两倍分辨率。 .NOTES 依赖 ffmpeg 和 ffprobe;缺失时命令会尝试自动安装。 #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, HelpMessage = '要转换的视频文件路径')] [string]$输入文件, [Parameter(Position = 1)] [string]$输出文件, [ValidateRange(0, 10000)] [int]$宽度 = 0, [ValidateRange(0, 10000)] [int]$高度 = 0, [ValidateScript({ if ($_ -gt 0) { $true } else { throw '-缩放率 必须是正数(1 不缩放,小于 1 缩小,大于 1 放大)' } })] [double]$缩放率 = 1, [ValidateRange(0.0, 1000.0)] [double]$帧率 = 0 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $script:输出外部进程信息 = $false $输入文件 = 规范化_用户路径参数 -Path $输入文件 if ($输出文件) { $输出文件 = 规范化_用户路径参数 -Path $输出文件 } 确保_ffmpeg_ffprobe_可用 if (-not (Test-Path -LiteralPath $输入文件)) { throw "文件不存在: $输入文件" } $输入绝对路径 = (Resolve-Path -LiteralPath $输入文件).Path # ── 探测输入 ───────────────────────────────────────────────── $媒体信息 = 获取_媒体信息 -Path $输入绝对路径 if (-not $媒体信息.Video) { throw "输入文件没有视频轨: $输入文件" } $源宽 = $媒体信息.Video.Width $源高 = $媒体信息.Video.Height $源编码 = $媒体信息.Video.Codec $时长秒 = [double]$媒体信息.Duration if (-not $源宽 -or -not $源高 -or $源宽 -le 0 -or $源高 -le 0) { throw "无法获取输入视频的分辨率: $输入文件" } # ── 计算目标尺寸(宽度/高度 与 缩放率 互斥)──────────────── $目标宽 = $源宽 $目标高 = $源高 $显式给了缩放率 = $PSBoundParameters.ContainsKey('缩放率') $显式给了宽高 = $PSBoundParameters.ContainsKey('宽度') -or $PSBoundParameters.ContainsKey('高度') if ($显式给了缩放率 -and $显式给了宽高) { throw '-缩放率 与 -宽度/-高度 不能同时指定:要么给出目标宽高(可只给一边,另一边按比例计算),要么给出缩放率。' } if ($显式给了缩放率) { if ($缩放率 -ne 1) { $目标宽 = [math]::Round($源宽 * $缩放率) $目标高 = [math]::Round($源高 * $缩放率) } } elseif ($宽度 -gt 0 -or $高度 -gt 0) { if ($宽度 -gt 0 -and $高度 -gt 0) { $目标宽 = $宽度 $目标高 = $高度 } elseif ($宽度 -gt 0) { $目标宽 = $宽度 $目标高 = [math]::Round($源高 * ([double]$宽度 / $源宽)) } else { $目标高 = $高度 $目标宽 = [math]::Round($源宽 * ([double]$高度 / $源高)) } } # ffmpeg scale 要求目标边长为偶数;取偶后误差不超过 1 像素 $目标宽 = [math]::Max(2, [int]($目标宽 / 2) * 2) $目标高 = [math]::Max(2, [int]($目标高 / 2) * 2) # ── 默认输出路径 ───────────────────────────────────────────── if (-not $输出文件) { $目录 = [System.IO.Path]::GetDirectoryName($输入绝对路径) $基础名 = [System.IO.Path]::GetFileNameWithoutExtension($输入绝对路径) $输出文件 = Join-Path $目录 "${基础名}.gif" } $输出绝对路径 = [System.IO.Path]::GetFullPath($输出文件) if ($输出绝对路径 -ieq $输入绝对路径) { throw '输出文件不能与输入文件相同,请换一个输出路径。' } $ffmpeg路径 = 解决_工具执行路径 -工具名 'ffmpeg' # ── 组装滤镜链(两遍使用同一链,保证调色板与画面一致)──────── # ffmpeg fps 滤镜要求小数点为英文句点;用 InvariantCulture 格式化,避免区域设置影响 $帧率文本 = $帧率.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture) $滤镜片段 = @() if ($帧率 -gt 0) { $滤镜片段 += "fps=$帧率文本" } if ($目标宽 -ne $源宽 -or $目标高 -ne $源高) { $滤镜片段 += ("scale={0}:{1}:flags=lanczos" -f $目标宽, $目标高) } $滤镜链 = $滤镜片段 -join ',' $pass1滤镜 = if ($滤镜链) { "$滤镜链,palettegen=stats_mode=diff" } else { 'palettegen=stats_mode=diff' } $pass2滤镜 = if ($滤镜链) { ("[0:v]{0}[v];[v][1:v]paletteuse=dither=bayer:bayer_scale=5" -f $滤镜链) } else { '[0:v][1:v]paletteuse=dither=bayer:bayer_scale=5' } # ── 打印摘要 ──────────────────────────────────────────────── Write-Host '' Write-Host '视频转 GIF(两遍调色板流程)' -ForegroundColor Cyan Write-Host (" 输入 : {0}" -f $输入绝对路径) Write-Host (" 视频轨 : {0} {1}x{2} 时长 {3}s" -f $源编码, $源宽, $源高, ([math]::Round($时长秒, 1))) if ($目标宽 -ne $源宽 -or $目标高 -ne $源高) { Write-Host (" 目标尺寸 : {0}x{1}" -f $目标宽, $目标高) } if ($帧率 -gt 0) { Write-Host (" 帧率 : {0} fps" -f $帧率文本) } Write-Host (" 输出 : {0}" -f $输出绝对路径) Write-Host '' # 调色板临时目录(Pass 1/2 共用),结束后清理 $临时目录 = Join-Path ([System.IO.Path]::GetTempPath()) ("视频工坊GIF_" + [Guid]::NewGuid().ToString('N')) $调色板文件 = Join-Path $临时目录 'palette.png' New-Item -ItemType Directory -Path $临时目录 -Force | Out-Null try { # Pass 1:全片分析生成调色板 Write-Host 'Pass 1 / 2 分析调色板...' -ForegroundColor DarkYellow $pass1参数 = @('-hide_banner', '-nostdin', '-y', '-i', $输入绝对路径, '-vf', $pass1滤镜, '-frames:v', '1', '-update', '1', $调色板文件) $旧错误偏好 = $ErrorActionPreference $ErrorActionPreference = 'SilentlyContinue' & $ffmpeg路径 @pass1参数 $退出码 = $LASTEXITCODE $ErrorActionPreference = $旧错误偏好 if ($退出码 -ne 0) { throw "转 GIF 失败:Pass 1 调色板分析出错(退出码 $退出码)" } if (-not (Test-Path -LiteralPath $调色板文件)) { throw '转 GIF 失败:Pass 1 未生成调色板文件。' } # Pass 2:用调色板量化输出 GIF Write-Host 'Pass 2 / 2 生成 GIF...' -ForegroundColor DarkYellow $pass2参数 = @('-hide_banner', '-nostdin', '-y', '-i', $输入绝对路径, '-i', $调色板文件, '-lavfi', $pass2滤镜, '-loop', '0', $输出绝对路径) $ErrorActionPreference = 'SilentlyContinue' & $ffmpeg路径 @pass2参数 $退出码 = $LASTEXITCODE $ErrorActionPreference = $旧错误偏好 if ($退出码 -ne 0) { throw "转 GIF 失败:Pass 2 生成出错(退出码 $退出码)" } } finally { Remove-Item -LiteralPath $临时目录 -Recurse -Force -ErrorAction SilentlyContinue } # ── 完成,报告输出 ────────────────────────────────────────── $实际字节 = (Get-Item -LiteralPath $输出绝对路径).Length Write-Host '' Write-Host '转换完成' -ForegroundColor Green Write-Host (" 输出文件 : {0}" -f $输出绝对路径) Write-Host (" 大小 : {0}" -f (格式化_字节到文本 $实际字节)) } |