diff --git a/SpineViewer/Dialogs/ExportDialog.Designer.cs b/SpineViewer/Dialogs/ExportDialog.Designer.cs index 23f505c..7d233ce 100644 --- a/SpineViewer/Dialogs/ExportDialog.Designer.cs +++ b/SpineViewer/Dialogs/ExportDialog.Designer.cs @@ -47,7 +47,7 @@ panel1.Location = new Point(0, 0); panel1.Name = "panel1"; panel1.Padding = new Padding(50, 15, 50, 10); - panel1.Size = new Size(710, 698); + panel1.Size = new Size(793, 754); panel1.TabIndex = 2; // // tableLayoutPanel1 @@ -65,7 +65,7 @@ tableLayoutPanel1.RowStyles.Add(new RowStyle()); tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F)); tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F)); - tableLayoutPanel1.Size = new Size(610, 673); + tableLayoutPanel1.Size = new Size(693, 729); tableLayoutPanel1.TabIndex = 0; // // propertyGrid_ExportArgs @@ -74,7 +74,7 @@ propertyGrid_ExportArgs.Location = new Point(3, 3); propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs"; propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized; - propertyGrid_ExportArgs.Size = new Size(604, 594); + propertyGrid_ExportArgs.Size = new Size(687, 650); propertyGrid_ExportArgs.TabIndex = 1; propertyGrid_ExportArgs.ToolbarVisible = false; // @@ -88,18 +88,18 @@ tableLayoutPanel2.Controls.Add(button_Ok, 0, 0); tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0); tableLayoutPanel2.Dock = DockStyle.Bottom; - tableLayoutPanel2.Location = new Point(3, 630); + tableLayoutPanel2.Location = new Point(3, 686); tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3); tableLayoutPanel2.Name = "tableLayoutPanel2"; tableLayoutPanel2.RowCount = 1; tableLayoutPanel2.RowStyles.Add(new RowStyle()); - tableLayoutPanel2.Size = new Size(604, 40); + tableLayoutPanel2.Size = new Size(687, 40); tableLayoutPanel2.TabIndex = 10; // // button_Ok // button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; - button_Ok.Location = new Point(160, 3); + button_Ok.Location = new Point(201, 3); button_Ok.Margin = new Padding(3, 3, 30, 3); button_Ok.Name = "button_Ok"; button_Ok.Size = new Size(112, 34); @@ -111,7 +111,7 @@ // button_Cancel // button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; - button_Cancel.Location = new Point(332, 3); + button_Cancel.Location = new Point(373, 3); button_Cancel.Margin = new Padding(30, 3, 3, 3); button_Cancel.Name = "button_Cancel"; button_Cancel.Size = new Size(112, 34); @@ -126,9 +126,8 @@ AutoScaleDimensions = new SizeF(11F, 24F); AutoScaleMode = AutoScaleMode.Font; CancelButton = button_Cancel; - ClientSize = new Size(710, 698); + ClientSize = new Size(793, 754); Controls.Add(panel1); - FormBorderStyle = FormBorderStyle.FixedDialog; Icon = (Icon)resources.GetObject("$this.Icon"); MaximizeBox = false; MinimizeBox = false; diff --git a/SpineViewer/Dialogs/ExportDialog.cs b/SpineViewer/Dialogs/ExportDialog.cs index 31b2d9b..25aae4a 100644 --- a/SpineViewer/Dialogs/ExportDialog.cs +++ b/SpineViewer/Dialogs/ExportDialog.cs @@ -11,7 +11,7 @@ using System.Reflection; namespace SpineViewer.Dialogs { - public partial class ExportDialog: Form + public partial class ExportDialog : Form { private readonly ExporterWrapper wrapper; @@ -64,7 +64,7 @@ namespace SpineViewer.Dialogs private void button_Ok_Click(object sender, EventArgs e) { if (wrapper.Exporter.Validate() is string error) - { + { MessagePopup.Info(error, "参数错误"); return; } diff --git a/SpineViewer/Exporter/AvifExporter.cs b/SpineViewer/Exporter/AvifExporter.cs new file mode 100644 index 0000000..d0795ec --- /dev/null +++ b/SpineViewer/Exporter/AvifExporter.cs @@ -0,0 +1,55 @@ +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter +{ + /// + /// MP4 导出参数 + /// + public class AvifExporter : FFmpegVideoExporter + { + public AvifExporter() + { + FPS = 12; + } + + public override string Format => "avif"; + + public override string Suffix => ".avif"; + + /// + /// 编码器 + /// + public string Codec { get; set; } = "av1_nvenc"; + + /// + /// CRF + /// + public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } + private int crf = 23; + + /// + /// 像素格式 + /// + public string PixelFormat { get; set; } = "yuv420p"; + + /// + /// 循环次数, 0 无限循环, 取值范围 [0, 65535] + /// + public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); } + private int loop = 0; + + public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}"; + + public override void SetOutputOptions(FFMpegArgumentOptions options) + { + base.SetOutputOptions(options); + options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}"); + } + } +} diff --git a/SpineViewer/Exporter/CustomExporter.cs b/SpineViewer/Exporter/CustomExporter.cs index d270b84..408e0cf 100644 --- a/SpineViewer/Exporter/CustomExporter.cs +++ b/SpineViewer/Exporter/CustomExporter.cs @@ -12,6 +12,11 @@ namespace SpineViewer.Exporter /// public class CustomExporter : FFmpegVideoExporter { + public CustomExporter() + { + CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数 + } + public override string Format => CustomFormat; public override string Suffix => CustomSuffix; diff --git a/SpineViewer/Exporter/GifExporter.cs b/SpineViewer/Exporter/GifExporter.cs index b33a291..22da00c 100644 --- a/SpineViewer/Exporter/GifExporter.cs +++ b/SpineViewer/Exporter/GifExporter.cs @@ -35,6 +35,12 @@ namespace SpineViewer.Exporter public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; } private byte alphaThreshold = 128; + /// + /// 循环次数, -1 不循环, 0 无限循环, 取值范围 [-1, 65535] + /// + public int Loop { get => loop; set => loop = Math.Clamp(value, -1, 65535); } + private int loop = 0; + public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}"; public override void SetOutputOptions(FFMpegArgumentOptions options) @@ -43,7 +49,7 @@ namespace SpineViewer.Exporter 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}\""; + var customArgs = $"-filter_complex \"{v};{s0};{s1}\" -loop {Loop}"; options.WithCustomArgument(customArgs); } } diff --git a/SpineViewer/Exporter/WebpExporter.cs b/SpineViewer/Exporter/WebpExporter.cs new file mode 100644 index 0000000..21c0f32 --- /dev/null +++ b/SpineViewer/Exporter/WebpExporter.cs @@ -0,0 +1,60 @@ +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter +{ + /// + /// MP4 导出参数 + /// + public class WebpExporter : FFmpegVideoExporter + { + public WebpExporter() + { + FPS = 12; + } + + public override string Format => "webp"; + + public override string Suffix => ".webp"; + + /// + /// 编码器 + /// + public string Codec { get; set; } = "libwebp"; + + /// + /// 是否无损 + /// + public bool Lossless { get; set; } = false; + + /// + /// 质量 + /// + public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); } + private int quality = 75; + + /// + /// 像素格式 + /// + public string PixelFormat { get; set; } = "yuva420p"; + + /// + /// 循环次数, 0 无限循环, 取值范围 [0, 65535] + /// + public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); } + private int loop = 0; + + public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}"; + + public override void SetOutputOptions(FFMpegArgumentOptions options) + { + base.SetOutputOptions(options); + options.WithVideoCodec(Codec).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}"); + } + } +} diff --git a/SpineViewer/PropertyGridWrappers/Exporter/AvifExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/AvifExporterWrapper.cs new file mode 100644 index 0000000..4857e50 --- /dev/null +++ b/SpineViewer/PropertyGridWrappers/Exporter/AvifExporterWrapper.cs @@ -0,0 +1,44 @@ +using SpineViewer.Exporter; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.PropertyGridWrappers.Exporter +{ + public class AvifExporterWrapper(AvifExporter exporter) : FFmpegVideoExporterWrapper(exporter) + { + [Browsable(false)] + public override AvifExporter Exporter => (AvifExporter)base.Exporter; + + /// + /// 编码器 + /// + [StringEnumConverter.StandardValues("av1_nvenc", "av1_amf", "libaom-av1", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器\n建议使用硬件加速, libaom-av1 速度非常非常非常慢")] + public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; } + + /// + /// CRF + /// + [Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")] + public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; } + + /// + /// 像素格式 + /// + [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")] + public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; } + + /// + /// 循环次数 + /// + [Category("[3] 格式参数"), DisplayName("循环次数"), Description("循环次数, 0 无限循环, 取值范围 [0, 65535]")] + public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; } + } +} diff --git a/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs index 261bcf5..fc77831 100644 --- a/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs +++ b/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs @@ -13,16 +13,29 @@ namespace SpineViewer.PropertyGridWrappers.Exporter [Browsable(false)] public override CustomExporter Exporter => (CustomExporter)base.Exporter; + [Browsable(false)] + public override string Format => Exporter.Format; + + [Browsable(false)] + public override string Suffix => Exporter.Suffix; + /// /// 文件格式 /// - [Category("[3] 自定义参数"), DisplayName("文件格式"), Description("文件格式")] - public string CustomFormat { get; set; } = "mp4"; + [Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")] + public string CustomFormat { get => Exporter.CustomFormat; set => Exporter.CustomFormat = value; } /// /// 文件名后缀 /// - [Category("[3] 自定义参数"), DisplayName("文件名后缀"), Description("文件名后缀")] - public string CustomSuffix { get; set; } = ".mp4"; + [Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")] + public string CustomSuffix { get => Exporter.CustomSuffix; set => Exporter.CustomSuffix = value; } + + /// + /// 文件名后缀 + /// + [Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数")] + public override string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; } + } } diff --git a/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs index c9f5677..6e95ba7 100644 --- a/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs +++ b/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs @@ -17,18 +17,18 @@ namespace SpineViewer.PropertyGridWrappers.Exporter /// 文件格式 /// [Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")] - public string Format => Exporter.Format; + public virtual string Format => Exporter.Format; /// /// 文件名后缀 /// [Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")] - public string Suffix => Exporter.Format; + public virtual string Suffix => Exporter.Suffix; /// /// 文件名后缀 /// [Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数, 除非很清楚自己在做什么, 否则请勿填写此参数")] - public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; } + public virtual string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; } } } diff --git a/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs index fc1ba69..d8d94b9 100644 --- a/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs +++ b/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs @@ -24,5 +24,11 @@ namespace SpineViewer.PropertyGridWrappers.Exporter /// [Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")] public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; } + + /// + /// 透明度阈值 + /// + [Category("[3] 格式参数"), DisplayName("循环次数"), Description("循环次数, -1 不循环, 0 无限循环, 取值范围 [-1, 65535]")] + public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; } } } diff --git a/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs index 5470098..2c00492 100644 --- a/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs +++ b/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs @@ -16,7 +16,7 @@ namespace SpineViewer.PropertyGridWrappers.Exporter /// /// 编码器 /// - [StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)] + [StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", "av1_nvenc", Customizable = true)] [TypeConverter(typeof(StringEnumConverter))] [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")] public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; } diff --git a/SpineViewer/PropertyGridWrappers/Exporter/WebpExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/WebpExporterWrapper.cs new file mode 100644 index 0000000..0f97e56 --- /dev/null +++ b/SpineViewer/PropertyGridWrappers/Exporter/WebpExporterWrapper.cs @@ -0,0 +1,50 @@ +using SpineViewer.Exporter; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.PropertyGridWrappers.Exporter +{ + public class WebpExporterWrapper(WebpExporter exporter) : FFmpegVideoExporterWrapper(exporter) + { + [Browsable(false)] + public override WebpExporter Exporter => (WebpExporter)base.Exporter; + + /// + /// 编码器 + /// + [StringEnumConverter.StandardValues("libwebp", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")] + public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; } + + /// + /// 是否无损 + /// + [Category("[3] 格式参数"), DisplayName("无损"), Description("-lossless, 0 表示有损, 1 表示无损")] + public bool Lossless { get => Exporter.Lossless; set => Exporter.Lossless = value; } + + /// + /// CRF + /// + [Category("[3] 格式参数"), DisplayName("质量"), Description("-quality, 取值范围 0-100, 默认值 75")] + public int Quality { get => Exporter.Quality; set => Exporter.Quality = value; } + + /// + /// 像素格式 + /// + [StringEnumConverter.StandardValues("yuv420p", "yuva420p", Customizable = true)] + [TypeConverter(typeof(StringEnumConverter))] + [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")] + public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; } + + /// + /// 透明度阈值 + /// + [Category("[3] 格式参数"), DisplayName("循环次数"), Description("循环次数, 0 无限循环, 取值范围 [0, 65535]")] + public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; } + } +}