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)
{