From e9accd13b319ad29ac819317c92026bd801e37c5 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 31 Mar 2025 17:30:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=80=E6=9C=89=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/Dialogs/OpenSpineDialog.cs | 2 +- SpineViewer/Exporter/ExportArgs.cs | 8 +- SpineViewer/Exporter/ExportHelper.cs | 11 +-- .../ExportArgs/CustomExportArgs.cs | 38 +++++++++ .../ExportArgs/FFmpegVideoExportArgs.cs | 59 +++++++++++++ .../ExportArgs/FrameExportArgs.cs | 2 +- .../ExportArgs/FrameSequenceExportArgs.cs | 2 +- .../ExportArgs/GifExportArgs.cs | 36 ++++---- .../ExportArgs/MkvExportArgs.cs | 57 +++++++++++++ .../ExportArgs/MovExportArgs.cs | 58 +++++++++++++ .../ExportArgs/Mp4ExportArgs.cs | 40 +++++++-- .../ExportArgs/VideoExportArgs.cs | 4 +- .../ExportArgs/WebmExportArgs.cs | 58 +++++++++++++ ...{GifExporter.cs => FFmpegVideoExporter.cs} | 46 +++++----- .../Implementations/Exporter/FrameExporter.cs | 4 +- .../Exporter/FrameSequenceExporter.cs | 4 +- .../Implementations/Exporter/Mp4Exporter.cs | 85 ------------------- SpineViewer/MainForm.Designer.cs | 52 +++++++----- SpineViewer/MainForm.cs | 11 +-- SpineViewer/TypeConverter.cs | 11 ++- 20 files changed, 411 insertions(+), 177 deletions(-) create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs rename SpineViewer/Exporter/Implementations/Exporter/{GifExporter.cs => FFmpegVideoExporter.cs} (58%) delete mode 100644 SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs diff --git a/SpineViewer/Dialogs/OpenSpineDialog.cs b/SpineViewer/Dialogs/OpenSpineDialog.cs index 354cadb..db9dbe5 100644 --- a/SpineViewer/Dialogs/OpenSpineDialog.cs +++ b/SpineViewer/Dialogs/OpenSpineDialog.cs @@ -65,7 +65,7 @@ namespace SpineViewer.Dialogs skelPath = Path.GetFullPath(skelPath); } - if (string.IsNullOrEmpty(atlasPath)) + if (string.IsNullOrWhiteSpace(atlasPath)) { atlasPath = null; } diff --git a/SpineViewer/Exporter/ExportArgs.cs b/SpineViewer/Exporter/ExportArgs.cs index d322f00..a40b51f 100644 --- a/SpineViewer/Exporter/ExportArgs.cs +++ b/SpineViewer/Exporter/ExportArgs.cs @@ -79,14 +79,14 @@ namespace SpineViewer.Exporter /// public virtual string? Validate() { - if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir)) + if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir)) return "输出文件夹无效"; - if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir)) + if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir)) return $"文件夹 {OutputDir} 不存在"; - if (ExportSingle && string.IsNullOrEmpty(OutputDir)) + if (ExportSingle && string.IsNullOrWhiteSpace(OutputDir)) return "导出单个时必须提供输出文件夹"; - OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir); + OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir); return null; } } diff --git a/SpineViewer/Exporter/ExportHelper.cs b/SpineViewer/Exporter/ExportHelper.cs index 0f08d62..2c6a17d 100644 --- a/SpineViewer/Exporter/ExportHelper.cs +++ b/SpineViewer/Exporter/ExportHelper.cs @@ -15,11 +15,12 @@ namespace SpineViewer.Exporter { Frame, FrameSequence, - GIF, - MKV, - MP4, - MOV, - WebM + Gif, + Mp4, + Webm, + Mkv, + Mov, + Custom, } /// diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs new file mode 100644 index 0000000..aad13c7 --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs @@ -0,0 +1,38 @@ +using FFMpegCore.Enums; +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// FFmpeg 自定义视频导出参数 + /// + [ExportImplementation(ExportType.Custom)] + public class CustomExportArgs : FFmpegVideoExportArgs + { + public CustomExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { } + + public override string Format => CustomFormat; + + public override string Suffix => CustomSuffix; + + public override string FileNameNoteSuffix => string.Empty; + + /// + /// 文件格式 + /// + [Category("[3] 自定义参数"), DisplayName("文件格式"), Description("文件格式")] + public string CustomFormat { get; set; } = "mp4"; + + /// + /// 文件名后缀 + /// + [Category("[3] 自定义参数"), DisplayName("文件名后缀"), Description("文件名后缀")] + public string CustomSuffix { get; set; } = ".mp4"; + } +} diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs new file mode 100644 index 0000000..dfb0b85 --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs @@ -0,0 +1,59 @@ +using FFMpegCore.Enums; +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// 使用 FFmpeg 视频导出参数 + /// + public abstract class FFmpegVideoExportArgs : VideoExportArgs + { + public FFmpegVideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { } + + /// + /// 文件格式 + /// + [Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("文件格式")] + public abstract string Format { get; } + + /// + /// 文件名后缀 + /// + [Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")] + public abstract string Suffix { get; } + + /// + /// 文件名后缀 + /// + [Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数, 除非很清楚自己在做什么, 否则请勿填写此参数")] + public string CustomArgument { get; set; } + + /// + /// 获取输出附加选项 + /// + public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument); + + /// + /// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符 + /// + [Browsable(false)] + public abstract string FileNameNoteSuffix { get; } + + public override string? Validate() + { + if (base.Validate() is string error) + return error; + if (string.IsNullOrWhiteSpace(Format)) + return "需要提供有效的格式"; + if (string.IsNullOrWhiteSpace(Suffix)) + return "需要提供有效的文件名后缀"; + return null; + } + } +} diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs index e939faf..ccd93cc 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs @@ -36,7 +36,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs /// 文件名后缀 /// [Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")] - public string FileSuffix { get => imageFormat.GetSuffix(); } + public string Suffix { get => imageFormat.GetSuffix(); } /// /// DPI diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs index 2d3a3c4..48db452 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs @@ -21,6 +21,6 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs /// [TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")] [Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")] - public string FileSuffix { get; set; } = ".png"; + public string Suffix { get; set; } = ".png"; } } diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs index 4e51467..ebe6b53 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs @@ -1,4 +1,5 @@ -using System; +using FFMpegCore; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -10,8 +11,8 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs /// /// GIF 导出参数 /// - [ExportImplementation(ExportType.GIF)] - public class GifExportArgs : VideoExportArgs + [ExportImplementation(ExportType.Gif)] + public class GifExportArgs : FFmpegVideoExportArgs { public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { @@ -22,33 +23,34 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs FPS = 12; } + public override string Format => "gif"; + + public override string Suffix => ".gif"; + /// /// 调色板最大颜色数量 /// - [Category("[2] GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")] + [Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")] public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); } private uint maxColors = 256; /// /// 透明度阈值 /// - [Category("[2] GIF 参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")] + [Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")] public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; } private byte alphaThreshold = 128; - /// - /// 获取构造好的 FFMpegCore 自定义参数 - /// - [Browsable(false)] - public string FFMpegCoreCustomArguments + public override void SetOutputOptions(FFMpegArgumentOptions options) { - get - { - var v = $"[0:v] split [s0][s1]"; - var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]"; - var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}"; - return $"-filter_complex \"{v};{s0};{s1}\""; - } + base.SetOutputOptions(options); + var v = $"[0:v] split [s0][s1]"; + var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]"; + var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}"; + var customArgs = $"-filter_complex \"{v};{s0};{s1}\""; + options.WithCustomArgument(customArgs); } + + public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}"; } } diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs new file mode 100644 index 0000000..d5bb08d --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs @@ -0,0 +1,57 @@ +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// MKV 导出参数 + /// + [ExportImplementation(ExportType.Mkv)] + public class MkvExportArgs : FFmpegVideoExportArgs + { + public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) + { + BackgroundColor = new(0, 255, 0, 0); + } + + public override string Format => "matroska"; + + public override string Suffix => ".mkv"; + + /// + /// 编码器 + /// + [StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")] + public string Codec { get; set; } = "libx265"; + + /// + /// CRF + /// + [Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")] + public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } + private int crf = 23; + + /// + /// 像素格式 + /// + [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")] + public string PixelFormat { get; set; } = "yuv444p"; + + public override void SetOutputOptions(FFMpegArgumentOptions options) + { + base.SetOutputOptions(options); + options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat); + } + + public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}"; + } +} diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs new file mode 100644 index 0000000..70f27ae --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs @@ -0,0 +1,58 @@ +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// MOV 导出参数 + /// + [ExportImplementation(ExportType.Mov)] + public class MovExportArgs : FFmpegVideoExportArgs + { + public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) + { + BackgroundColor = new(0, 255, 0, 0); + } + + public override string Format => "mov"; + + public override string Suffix => ".mov"; + + /// + /// 编码器 + /// + [StringEnumConverter.StandardValues("prores_ks", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")] + public string Codec { get; set; } = "prores_ks"; + + /// + /// 预设 + /// + [StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "444xq")] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("预设"), Description("-profile, 预设配置")] + public string Profile { get; set; } = "auto"; + + /// + /// 像素格式 + /// + [StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")] + public string PixelFormat { get; set; } = "yuva444p10le"; + + public override void SetOutputOptions(FFMpegArgumentOptions options) + { + base.SetOutputOptions(options); + options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat); + } + + public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}"; + } +} diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs index 9aa5f26..61cdc67 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs @@ -1,4 +1,5 @@ -using System; +using FFMpegCore; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -10,26 +11,47 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs /// /// MP4 导出参数 /// - [ExportImplementation(ExportType.MP4)] - public class Mp4ExportArgs : VideoExportArgs + [ExportImplementation(ExportType.Mp4)] + public class Mp4ExportArgs : FFmpegVideoExportArgs { - public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) + public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { - // MP4 默认用绿幕 BackgroundColor = new(0, 255, 0, 0); } + public override string Format => "mp4"; + + public override string Suffix => ".mp4"; + + /// + /// 编码器 + /// + [StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")] + public string Codec { get; set; } = "libx264"; + /// /// CRF /// - [Category("[2] MP4 参数"), DisplayName("CRF"), Description("Constant Rate Factor, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")] + [Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")] public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } private int crf = 23; /// - /// 编码器 TODO: 增加其他编码器 + /// 像素格式 /// - [Category("[2] MP4 参数"), DisplayName("编码器"), Description("要使用的编码器")] - public string Codec { get => "libx264"; } + [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")] + public string PixelFormat { get; set; } = "yuv444p"; + + public override void SetOutputOptions(FFMpegArgumentOptions options) + { + base.SetOutputOptions(options); + options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat); + } + + public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}"; } } diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs index 5b9c6cb..8d1f685 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs @@ -1,4 +1,6 @@ -using System; +using FFMpegCore.Enums; +using FFMpegCore; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs new file mode 100644 index 0000000..d05a923 --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs @@ -0,0 +1,58 @@ +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// WebM 导出参数 + /// + [ExportImplementation(ExportType.Webm)] + public class WebmExportArgs : FFmpegVideoExportArgs + { + public WebmExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) + { + // 默认用透明黑背景 + BackgroundColor = new(0, 0, 0, 0); + } + + public override string Format => "webm"; + + public override string Suffix => ".webm"; + + /// + /// 编码器 + /// + [StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("编码器"), Description("要使用的编码器")] + public string Codec { get; set; } = "libvpx-vp9"; + + /// + /// CRF + /// + [Category("[3] 格式参数"), DisplayName("CRF"), Description("Constant Rate Factor, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")] + public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } + private int crf = 23; + + /// + /// 像素格式 + /// + [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("像素格式"), Description("要使用的像素格式")] + public string PixelFormat { get; set; } = "yuva420p"; + + public override void SetOutputOptions(FFMpegArgumentOptions options) + { + base.SetOutputOptions(options); + options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat); + } + + public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}"; + } +} diff --git a/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs b/SpineViewer/Exporter/Implementations/Exporter/FFmpegVideoExporter.cs similarity index 58% rename from SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs rename to SpineViewer/Exporter/Implementations/Exporter/FFmpegVideoExporter.cs index 127a7f6..c30d253 100644 --- a/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs +++ b/SpineViewer/Exporter/Implementations/Exporter/FFmpegVideoExporter.cs @@ -13,49 +13,57 @@ using System.Diagnostics; namespace SpineViewer.Exporter.Implementations.Exporter { /// - /// GIF 动图导出器 + /// 使用 FFmpeg 的视频导出器 /// - [ExportImplementation(ExportType.GIF)] - public class GifExporter : VideoExporter + [ExportImplementation(ExportType.Gif)] + [ExportImplementation(ExportType.Mp4)] + [ExportImplementation(ExportType.Webm)] + [ExportImplementation(ExportType.Mkv)] + [ExportImplementation(ExportType.Mov)] + [ExportImplementation(ExportType.Custom)] + public class FFmpegVideoExporter : VideoExporter { - public GifExporter(GifExportArgs exportArgs) : base(exportArgs) { } + public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { } protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) { - var args = (GifExportArgs)ExportArgs; + var args = (FFmpegVideoExportArgs)ExportArgs; + var noteSuffix = args.FileNameNoteSuffix; + if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}"; + + var filename = $"{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}"; // 导出单个时必定提供输出文件夹 - var filename = $"{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif"; var savePath = Path.Combine(args.OutputDir, filename); var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS }; try { - var ffmpegArgs = FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(savePath, true, options => options - .ForceFormat("gif") - .WithCustomArgument(args.FFMpegCoreCustomArguments)); + var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, args.SetOutputOptions); - logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); + logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments); ffmpegArgs.ProcessSynchronously(); } catch (Exception ex) { logger.Error(ex.ToString()); - logger.Error("Failed to export gif {}", savePath); + logger.Error("Failed to export {} {}", args.Format, savePath); } } protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) { - var args = (GifExportArgs)ExportArgs; + var args = (FFmpegVideoExportArgs)ExportArgs; + var noteSuffix = args.FileNameNoteSuffix; + if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}"; + foreach (var spine in spinesToRender) { if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 + var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}"; + // 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下 - var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif"; var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename); var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS }; @@ -63,17 +71,15 @@ namespace SpineViewer.Exporter.Implementations.Exporter { var ffmpegArgs = FFMpegArguments .FromPipeInput(videoFramesSource) - .OutputToFile(savePath, true, options => options - .ForceFormat("gif") - .WithCustomArgument(args.FFMpegCoreCustomArguments)); + .OutputToFile(savePath, true, args.SetOutputOptions); - logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); + logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments); ffmpegArgs.ProcessSynchronously(); } catch (Exception ex) { logger.Error(ex.ToString()); - logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath); + logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath); } } } diff --git a/SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs b/SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs index 1e76a31..c0ed723 100644 --- a/SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs +++ b/SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs @@ -22,7 +22,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter var args = (FrameExportArgs)ExportArgs; // 导出单个时必定提供输出文件夹 - var filename = $"frame_{timestamp}{args.FileSuffix}"; + var filename = $"frame_{timestamp}{args.Suffix}"; var savePath = Path.Combine(args.OutputDir, filename); worker?.ReportProgress(0, $"已处理 0/1"); @@ -55,7 +55,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter var spine = spinesToRender[i]; // 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹 - var filename = $"{spine.Name}_{timestamp}{args.FileSuffix}"; + var filename = $"{spine.Name}_{timestamp}{args.Suffix}"; var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename); try diff --git a/SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs b/SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs index c51021f..8609672 100644 --- a/SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs +++ b/SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs @@ -28,7 +28,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter int frameIdx = 0; foreach (var frame in GetFrames(spinesToRender, worker)) { - var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}"; + var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}"; var savePath = Path.Combine(saveDir, filename); try @@ -63,7 +63,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter int frameIdx = 0; foreach (var frame in GetFrames(spine, worker)) { - var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}"; + var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}"; var savePath = Path.Combine(saveDir, filename); try diff --git a/SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs b/SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs deleted file mode 100644 index 0c77458..0000000 --- a/SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs +++ /dev/null @@ -1,85 +0,0 @@ -using FFMpegCore.Pipes; -using FFMpegCore; -using SpineViewer.Exporter.Implementations.ExportArgs; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FFMpegCore.Arguments; -using System.Diagnostics; - -namespace SpineViewer.Exporter.Implementations.Exporter -{ - /// - /// MP4 导出器 - /// - [ExportImplementation(ExportType.MP4)] - public class Mp4Exporter : VideoExporter - { - public Mp4Exporter(Mp4ExportArgs exportArgs) : base(exportArgs) { } - - protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) - { - var args = (Mp4ExportArgs)ExportArgs; - - // 导出单个时必定提供输出文件夹 - var filename = $"{timestamp}_{args.FPS:f0}_{args.CRF}.mp4"; - var savePath = Path.Combine(args.OutputDir, filename); - - var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS }; - try - { - var ffmpegArgs = FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(savePath, true, options => options - .ForceFormat("mp4") - .WithVideoCodec(args.Codec) - .WithConstantRateFactor(args.CRF) - .WithFastStart()); - - logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); - ffmpegArgs.ProcessSynchronously(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - logger.Error("Failed to export mp4 {}", savePath); - } - } - - protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) - { - var args = (Mp4ExportArgs)ExportArgs; - foreach (var spine in spinesToRender) - { - if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 - - // 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下 - var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.CRF}.mp4"; - var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename); - - var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS }; - try - { - var ffmpegArgs = FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(savePath, true, options => options - .ForceFormat("mp4") - .WithVideoCodec(args.Codec) - .WithConstantRateFactor(args.CRF) - .WithFastStart()); - - logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); - ffmpegArgs.ProcessSynchronously(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - logger.Error("Failed to export mp4 {} {}", savePath, spine.SkelPath); - } - } - } - } -} diff --git a/SpineViewer/MainForm.Designer.cs b/SpineViewer/MainForm.Designer.cs index b50cd51..ff3ebfd 100644 --- a/SpineViewer/MainForm.Designer.cs +++ b/SpineViewer/MainForm.Designer.cs @@ -43,6 +43,7 @@ toolStripMenuItem_ExportMp4 = new ToolStripMenuItem(); toolStripMenuItem_ExportMov = new ToolStripMenuItem(); toolStripMenuItem_ExportWebm = new ToolStripMenuItem(); + toolStripMenuItem_ExportCustom = new ToolStripMenuItem(); toolStripSeparator2 = new ToolStripSeparator(); toolStripMenuItem_Exit = new ToolStripMenuItem(); toolStripMenuItem_Tool = new ToolStripMenuItem(); @@ -132,7 +133,7 @@ // // toolStripMenuItem_Export // - toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm }); + toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportCustom }); toolStripMenuItem_Export.Name = "toolStripMenuItem_Export"; toolStripMenuItem_Export.Size = new Size(270, 34); toolStripMenuItem_Export.Text = "导出(&E)"; @@ -140,55 +141,59 @@ // toolStripMenuItem_ExportFrame // toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame"; - toolStripMenuItem_ExportFrame.Size = new Size(270, 34); + toolStripMenuItem_ExportFrame.Size = new Size(288, 34); toolStripMenuItem_ExportFrame.Text = "单帧画面..."; toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportFrameSequence // toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence"; - toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34); + toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34); toolStripMenuItem_ExportFrameSequence.Text = "帧序列..."; toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportGif // toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif"; - toolStripMenuItem_ExportGif.Size = new Size(270, 34); + toolStripMenuItem_ExportGif.Size = new Size(288, 34); toolStripMenuItem_ExportGif.Text = "GIF..."; toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportMkv // toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv"; - toolStripMenuItem_ExportMkv.Size = new Size(270, 34); - toolStripMenuItem_ExportMkv.Text = "MKV"; - toolStripMenuItem_ExportMkv.Visible = false; + toolStripMenuItem_ExportMkv.Size = new Size(288, 34); + toolStripMenuItem_ExportMkv.Text = "MKV..."; toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportMp4 // toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4"; - toolStripMenuItem_ExportMp4.Size = new Size(270, 34); + toolStripMenuItem_ExportMp4.Size = new Size(288, 34); toolStripMenuItem_ExportMp4.Text = "MP4..."; toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportMov // toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov"; - toolStripMenuItem_ExportMov.Size = new Size(270, 34); + toolStripMenuItem_ExportMov.Size = new Size(288, 34); toolStripMenuItem_ExportMov.Text = "MOV..."; - toolStripMenuItem_ExportMov.Visible = false; toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click; // // toolStripMenuItem_ExportWebm // toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm"; - toolStripMenuItem_ExportWebm.Size = new Size(270, 34); + toolStripMenuItem_ExportWebm.Size = new Size(288, 34); toolStripMenuItem_ExportWebm.Text = "WebM..."; - toolStripMenuItem_ExportWebm.Visible = false; toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click; // + // toolStripMenuItem_ExportCustom + // + toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom"; + toolStripMenuItem_ExportCustom.Size = new Size(288, 34); + toolStripMenuItem_ExportCustom.Text = "FFmpeg 自定义导出..."; + toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_Export_Click; + // // toolStripSeparator2 // toolStripSeparator2.Name = "toolStripSeparator2"; @@ -266,7 +271,7 @@ rtbLog.Margin = new Padding(3, 2, 3, 2); rtbLog.Name = "rtbLog"; rtbLog.ReadOnly = true; - rtbLog.Size = new Size(1758, 134); + rtbLog.Size = new Size(1758, 146); rtbLog.TabIndex = 0; rtbLog.Text = ""; rtbLog.WordWrap = false; @@ -290,7 +295,7 @@ splitContainer_MainForm.Panel2.Controls.Add(rtbLog); splitContainer_MainForm.Panel2.Cursor = Cursors.Default; splitContainer_MainForm.Size = new Size(1758, 1097); - splitContainer_MainForm.SplitterDistance = 955; + splitContainer_MainForm.SplitterDistance = 943; splitContainer_MainForm.SplitterWidth = 8; splitContainer_MainForm.TabIndex = 3; splitContainer_MainForm.TabStop = false; @@ -314,7 +319,7 @@ // splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview); splitContainer_Functional.Panel2.Cursor = Cursors.Default; - splitContainer_Functional.Size = new Size(1758, 955); + splitContainer_Functional.Size = new Size(1758, 943); splitContainer_Functional.SplitterDistance = 759; splitContainer_Functional.SplitterWidth = 8; splitContainer_Functional.TabIndex = 2; @@ -338,7 +343,7 @@ // splitContainer_Information.Panel2.Controls.Add(splitContainer_Config); splitContainer_Information.Panel2.Cursor = Cursors.Default; - splitContainer_Information.Size = new Size(759, 955); + splitContainer_Information.Size = new Size(759, 943); splitContainer_Information.SplitterDistance = 354; splitContainer_Information.SplitterWidth = 8; splitContainer_Information.TabIndex = 1; @@ -352,7 +357,7 @@ groupBox_SkelList.Dock = DockStyle.Fill; groupBox_SkelList.Location = new Point(0, 0); groupBox_SkelList.Name = "groupBox_SkelList"; - groupBox_SkelList.Size = new Size(354, 955); + groupBox_SkelList.Size = new Size(354, 943); groupBox_SkelList.TabIndex = 0; groupBox_SkelList.TabStop = false; groupBox_SkelList.Text = "模型列表"; @@ -363,7 +368,7 @@ spineListView.Location = new Point(3, 26); spineListView.Name = "spineListView"; spineListView.PropertyGrid = propertyGrid_Spine; - spineListView.Size = new Size(348, 926); + spineListView.Size = new Size(348, 914); spineListView.TabIndex = 0; // // propertyGrid_Spine @@ -372,7 +377,7 @@ propertyGrid_Spine.HelpVisible = false; propertyGrid_Spine.Location = new Point(3, 26); propertyGrid_Spine.Name = "propertyGrid_Spine"; - propertyGrid_Spine.Size = new Size(391, 592); + propertyGrid_Spine.Size = new Size(391, 580); propertyGrid_Spine.TabIndex = 0; propertyGrid_Spine.ToolbarVisible = false; propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged; @@ -395,7 +400,7 @@ // splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig); splitContainer_Config.Panel2.Cursor = Cursors.Default; - splitContainer_Config.Size = new Size(397, 955); + splitContainer_Config.Size = new Size(397, 943); splitContainer_Config.SplitterDistance = 326; splitContainer_Config.SplitterWidth = 8; splitContainer_Config.TabIndex = 0; @@ -431,7 +436,7 @@ groupBox_SkelConfig.Dock = DockStyle.Fill; groupBox_SkelConfig.Location = new Point(0, 0); groupBox_SkelConfig.Name = "groupBox_SkelConfig"; - groupBox_SkelConfig.Size = new Size(397, 621); + groupBox_SkelConfig.Size = new Size(397, 609); groupBox_SkelConfig.TabIndex = 0; groupBox_SkelConfig.TabStop = false; groupBox_SkelConfig.Text = "模型参数"; @@ -442,7 +447,7 @@ groupBox_Preview.Dock = DockStyle.Fill; groupBox_Preview.Location = new Point(0, 0); groupBox_Preview.Name = "groupBox_Preview"; - groupBox_Preview.Size = new Size(991, 955); + groupBox_Preview.Size = new Size(991, 943); groupBox_Preview.TabIndex = 1; groupBox_Preview.TabStop = false; groupBox_Preview.Text = "预览画面"; @@ -453,7 +458,7 @@ spinePreviewer.Location = new Point(3, 26); spinePreviewer.Name = "spinePreviewer"; spinePreviewer.PropertyGrid = propertyGrid_Previewer; - spinePreviewer.Size = new Size(985, 926); + spinePreviewer.Size = new Size(985, 914); spinePreviewer.SpineListView = spineListView; spinePreviewer.TabIndex = 0; // @@ -553,5 +558,6 @@ private ToolStripMenuItem toolStripMenuItem_ExportMov; private ToolStripMenuItem toolStripMenuItem_ExportMkv; private ToolStripMenuItem toolStripMenuItem_ExportWebm; + private ToolStripMenuItem toolStripMenuItem_ExportCustom; } } diff --git a/SpineViewer/MainForm.cs b/SpineViewer/MainForm.cs index 8d32a1c..0c7b803 100644 --- a/SpineViewer/MainForm.cs +++ b/SpineViewer/MainForm.cs @@ -24,11 +24,12 @@ namespace SpineViewer // 在此处将导出菜单需要的类绑定起来 toolStripMenuItem_ExportFrame.Tag = ExportType.Frame; toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence; - toolStripMenuItem_ExportGif.Tag = ExportType.GIF; - toolStripMenuItem_ExportMkv.Tag = ExportType.MKV; - toolStripMenuItem_ExportMp4.Tag = ExportType.MP4; - toolStripMenuItem_ExportMov.Tag = ExportType.MOV; - toolStripMenuItem_ExportWebm.Tag = ExportType.WebM; + toolStripMenuItem_ExportGif.Tag = ExportType.Gif; + toolStripMenuItem_ExportMkv.Tag = ExportType.Mkv; + toolStripMenuItem_ExportMp4.Tag = ExportType.Mp4; + toolStripMenuItem_ExportMov.Tag = ExportType.Mov; + toolStripMenuItem_ExportWebm.Tag = ExportType.Webm; + toolStripMenuItem_ExportCustom.Tag = ExportType.Custom; // 执行一些初始化工作 try diff --git a/SpineViewer/TypeConverter.cs b/SpineViewer/TypeConverter.cs index 6d25d2a..4192246 100644 --- a/SpineViewer/TypeConverter.cs +++ b/SpineViewer/TypeConverter.cs @@ -61,6 +61,11 @@ namespace SpineViewer public ReadOnlyCollection StandardValues { get; private set; } private readonly List standardValues = []; + /// + /// 是否允许用户自定义 + /// + public bool Customizable { get; set; } = false; + /// /// 字符串标准值列表 /// @@ -74,7 +79,11 @@ namespace SpineViewer public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true; - public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true; + public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) + { + var customizable = context?.PropertyDescriptor?.Attributes.OfType().FirstOrDefault()?.Customizable ?? false; + return !customizable; + } public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) {