增加所有导出格式

This commit is contained in:
ww-rm
2025-03-31 17:30:20 +08:00
parent 9e27a19258
commit e9accd13b3
20 changed files with 411 additions and 177 deletions

View File

@@ -65,7 +65,7 @@ namespace SpineViewer.Dialogs
skelPath = Path.GetFullPath(skelPath); skelPath = Path.GetFullPath(skelPath);
} }
if (string.IsNullOrEmpty(atlasPath)) if (string.IsNullOrWhiteSpace(atlasPath))
{ {
atlasPath = null; atlasPath = null;
} }

View File

@@ -79,14 +79,14 @@ namespace SpineViewer.Exporter
/// </summary> /// </summary>
public virtual string? Validate() public virtual string? Validate()
{ {
if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir)) if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效"; return "输出文件夹无效";
if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir)) if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在"; return $"文件夹 {OutputDir} 不存在";
if (ExportSingle && string.IsNullOrEmpty(OutputDir)) if (ExportSingle && string.IsNullOrWhiteSpace(OutputDir))
return "导出单个时必须提供输出文件夹"; return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir); OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null; return null;
} }
} }

View File

@@ -15,11 +15,12 @@ namespace SpineViewer.Exporter
{ {
Frame, Frame,
FrameSequence, FrameSequence,
GIF, Gif,
MKV, Mp4,
MP4, Webm,
MOV, Mkv,
WebM Mov,
Custom,
} }
/// <summary> /// <summary>

View File

@@ -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
{
/// <summary>
/// FFmpeg 自定义视频导出参数
/// </summary>
[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;
/// <summary>
/// 文件格式
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomSuffix { get; set; } = ".mp4";
}
}

View File

@@ -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
{
/// <summary>
/// 使用 FFmpeg 视频导出参数
/// </summary>
public abstract class FFmpegVideoExportArgs : VideoExportArgs
{
public FFmpegVideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public abstract string Format { get; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public abstract string Suffix { get; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description(" FFmpeg , , ")]
public string CustomArgument { get; set; }
/// <summary>
/// 获取输出附加选项
/// </summary>
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
/// <summary>
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
/// </summary>
[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;
}
}
}

View File

@@ -36,7 +36,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// 文件名后缀 /// 文件名后缀
/// </summary> /// </summary>
[Category("[1] "), DisplayName(""), Description("")] [Category("[1] "), DisplayName(""), Description("")]
public string FileSuffix { get => imageFormat.GetSuffix(); } public string Suffix { get => imageFormat.GetSuffix(); }
/// <summary> /// <summary>
/// DPI /// DPI

View File

@@ -21,6 +21,6 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// </summary> /// </summary>
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")] [TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")] [Category("[2] "), DisplayName(""), Description("")]
public string FileSuffix { get; set; } = ".png"; public string Suffix { get; set; } = ".png";
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using FFMpegCore;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
@@ -10,8 +11,8 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// GIF 导出参数 /// GIF 导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.GIF)] [ExportImplementation(ExportType.Gif)]
public class GifExportArgs : VideoExportArgs public class GifExportArgs : FFmpegVideoExportArgs
{ {
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) 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; FPS = 12;
} }
public override string Format => "gif";
public override string Suffix => ".gif";
/// <summary> /// <summary>
/// 调色板最大颜色数量 /// 调色板最大颜色数量
/// </summary> /// </summary>
[Category("[2] GIF "), DisplayName(""), Description("使, ")] [Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); } public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
private uint maxColors = 256; private uint maxColors = 256;
/// <summary> /// <summary>
/// 透明度阈值 /// 透明度阈值
/// </summary> /// </summary>
[Category("[2] GIF "), DisplayName(""), Description("")] [Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; } public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
private byte alphaThreshold = 128; private byte alphaThreshold = 128;
/// <summary> public override void SetOutputOptions(FFMpegArgumentOptions options)
/// 获取构造好的 FFMpegCore 自定义参数
/// </summary>
[Browsable(false)]
public string FFMpegCoreCustomArguments
{ {
get base.SetOutputOptions(options);
{ var v = $"[0:v] split [s0][s1]";
var v = $"[0:v] split [s0][s1]"; var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]";
var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]"; var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}";
var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}"; var customArgs = $"-filter_complex \"{v};{s0};{s1}\"";
return $"-filter_complex \"{v};{s0};{s1}\""; options.WithCustomArgument(customArgs);
}
} }
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
} }
} }

View File

@@ -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
{
/// <summary>
/// MKV 导出参数
/// </summary>
[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";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "libx265";
/// <summary>
/// CRF
/// </summary>
[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;
/// <summary>
/// 像素格式
/// </summary>
[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}";
}
}

View File

@@ -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
{
/// <summary>
/// MOV 导出参数
/// </summary>
[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";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "prores_ks";
/// <summary>
/// 预设
/// </summary>
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "444xq")]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-profile, ")]
public string Profile { get; set; } = "auto";
/// <summary>
/// 像素格式
/// </summary>
[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}";
}
}

View File

@@ -1,4 +1,5 @@
using System; using FFMpegCore;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
@@ -10,26 +11,47 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// MP4 导出参数 /// MP4 导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.MP4)] [ExportImplementation(ExportType.Mp4)]
public class Mp4ExportArgs : VideoExportArgs 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); BackgroundColor = new(0, 255, 0, 0);
} }
public override string Format => "mp4";
public override string Suffix => ".mp4";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "libx264";
/// <summary> /// <summary>
/// CRF /// CRF
/// </summary> /// </summary>
[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); } public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23; private int crf = 23;
/// <summary> /// <summary>
/// 编码器 TODO: 增加其他编码器 /// 像素格式
/// </summary> /// </summary>
[Category("[2] MP4 "), DisplayName(""), Description("使")] [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
public string Codec { get => "libx264"; } [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}";
} }
} }

View File

@@ -1,4 +1,6 @@
using System; using FFMpegCore.Enums;
using FFMpegCore;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;

View File

@@ -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
{
/// <summary>
/// WebM 导出参数
/// </summary>
[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";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "libvpx-vp9";
/// <summary>
/// CRF
/// </summary>
[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;
/// <summary>
/// 像素格式
/// </summary>
[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}";
}
}

View File

@@ -13,49 +13,57 @@ using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter namespace SpineViewer.Exporter.Implementations.Exporter
{ {
/// <summary> /// <summary>
/// GIF 动图导出器 /// 使用 FFmpeg 的视频导出器
/// </summary> /// </summary>
[ExportImplementation(ExportType.GIF)] [ExportImplementation(ExportType.Gif)]
public class GifExporter : VideoExporter [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) 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 savePath = Path.Combine(args.OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS }; var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
try try
{ {
var ffmpegArgs = FFMpegArguments var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, args.SetOutputOptions);
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("gif")
.WithCustomArgument(args.FFMpegCoreCustomArguments));
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously(); ffmpegArgs.ProcessSynchronously();
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex.ToString()); 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) 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) foreach (var spine in spinesToRender)
{ {
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 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 savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS }; var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
@@ -63,17 +71,15 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{ {
var ffmpegArgs = FFMpegArguments var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource) .FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options .OutputToFile(savePath, true, args.SetOutputOptions);
.ForceFormat("gif")
.WithCustomArgument(args.FFMpegCoreCustomArguments));
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments); logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously(); ffmpegArgs.ProcessSynchronously();
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex.ToString()); logger.Error(ex.ToString());
logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath); logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath);
} }
} }
} }

View File

@@ -22,7 +22,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var args = (FrameExportArgs)ExportArgs; var args = (FrameExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹 // 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{args.FileSuffix}"; var filename = $"frame_{timestamp}{args.Suffix}";
var savePath = Path.Combine(args.OutputDir, filename); var savePath = Path.Combine(args.OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1"); worker?.ReportProgress(0, $"已处理 0/1");
@@ -55,7 +55,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var spine = spinesToRender[i]; 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); var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
try try

View File

@@ -28,7 +28,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
int frameIdx = 0; int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker)) 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); var savePath = Path.Combine(saveDir, filename);
try try
@@ -63,7 +63,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
int frameIdx = 0; int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker)) 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); var savePath = Path.Combine(saveDir, filename);
try try

View File

@@ -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
{
/// <summary>
/// MP4 导出器
/// </summary>
[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);
}
}
}
}
}

View File

@@ -43,6 +43,7 @@
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem(); toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
toolStripMenuItem_ExportMov = new ToolStripMenuItem(); toolStripMenuItem_ExportMov = new ToolStripMenuItem();
toolStripMenuItem_ExportWebm = new ToolStripMenuItem(); toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
toolStripMenuItem_ExportCustom = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator(); toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_Exit = new ToolStripMenuItem(); toolStripMenuItem_Exit = new ToolStripMenuItem();
toolStripMenuItem_Tool = new ToolStripMenuItem(); toolStripMenuItem_Tool = new ToolStripMenuItem();
@@ -132,7 +133,7 @@
// //
// toolStripMenuItem_Export // 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.Name = "toolStripMenuItem_Export";
toolStripMenuItem_Export.Size = new Size(270, 34); toolStripMenuItem_Export.Size = new Size(270, 34);
toolStripMenuItem_Export.Text = "导出(&E)"; toolStripMenuItem_Export.Text = "导出(&E)";
@@ -140,55 +141,59 @@
// toolStripMenuItem_ExportFrame // toolStripMenuItem_ExportFrame
// //
toolStripMenuItem_ExportFrame.Name = "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.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportFrameSequence // toolStripMenuItem_ExportFrameSequence
// //
toolStripMenuItem_ExportFrameSequence.Name = "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.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportGif // toolStripMenuItem_ExportGif
// //
toolStripMenuItem_ExportGif.Name = "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.Text = "GIF...";
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportMkv // toolStripMenuItem_ExportMkv
// //
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv"; toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(270, 34); toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
toolStripMenuItem_ExportMkv.Text = "MKV"; toolStripMenuItem_ExportMkv.Text = "MKV...";
toolStripMenuItem_ExportMkv.Visible = false;
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportMp4 // toolStripMenuItem_ExportMp4
// //
toolStripMenuItem_ExportMp4.Name = "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.Text = "MP4...";
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportMov // toolStripMenuItem_ExportMov
// //
toolStripMenuItem_ExportMov.Name = "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.Text = "MOV...";
toolStripMenuItem_ExportMov.Visible = false;
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportWebm // toolStripMenuItem_ExportWebm
// //
toolStripMenuItem_ExportWebm.Name = "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.Text = "WebM...";
toolStripMenuItem_ExportWebm.Visible = false;
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click; 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
// //
toolStripSeparator2.Name = "toolStripSeparator2"; toolStripSeparator2.Name = "toolStripSeparator2";
@@ -266,7 +271,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2); rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog"; rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true; rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1758, 134); rtbLog.Size = new Size(1758, 146);
rtbLog.TabIndex = 0; rtbLog.TabIndex = 0;
rtbLog.Text = ""; rtbLog.Text = "";
rtbLog.WordWrap = false; rtbLog.WordWrap = false;
@@ -290,7 +295,7 @@
splitContainer_MainForm.Panel2.Controls.Add(rtbLog); splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default; splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1758, 1097); splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 955; splitContainer_MainForm.SplitterDistance = 943;
splitContainer_MainForm.SplitterWidth = 8; splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3; splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false; splitContainer_MainForm.TabStop = false;
@@ -314,7 +319,7 @@
// //
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview); splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default; 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.SplitterDistance = 759;
splitContainer_Functional.SplitterWidth = 8; splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2; splitContainer_Functional.TabIndex = 2;
@@ -338,7 +343,7 @@
// //
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config); splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default; 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.SplitterDistance = 354;
splitContainer_Information.SplitterWidth = 8; splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1; splitContainer_Information.TabIndex = 1;
@@ -352,7 +357,7 @@
groupBox_SkelList.Dock = DockStyle.Fill; groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0); groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList"; 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.TabIndex = 0;
groupBox_SkelList.TabStop = false; groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表"; groupBox_SkelList.Text = "模型列表";
@@ -363,7 +368,7 @@
spineListView.Location = new Point(3, 26); spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView"; spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine; spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(348, 926); spineListView.Size = new Size(348, 914);
spineListView.TabIndex = 0; spineListView.TabIndex = 0;
// //
// propertyGrid_Spine // propertyGrid_Spine
@@ -372,7 +377,7 @@
propertyGrid_Spine.HelpVisible = false; propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26); propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine"; 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.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false; propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged; propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -395,7 +400,7 @@
// //
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig); splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default; 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.SplitterDistance = 326;
splitContainer_Config.SplitterWidth = 8; splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0; splitContainer_Config.TabIndex = 0;
@@ -431,7 +436,7 @@
groupBox_SkelConfig.Dock = DockStyle.Fill; groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0); groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig"; 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.TabIndex = 0;
groupBox_SkelConfig.TabStop = false; groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数"; groupBox_SkelConfig.Text = "模型参数";
@@ -442,7 +447,7 @@
groupBox_Preview.Dock = DockStyle.Fill; groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0); groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview"; 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.TabIndex = 1;
groupBox_Preview.TabStop = false; groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面"; groupBox_Preview.Text = "预览画面";
@@ -453,7 +458,7 @@
spinePreviewer.Location = new Point(3, 26); spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer"; spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer; spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(985, 926); spinePreviewer.Size = new Size(985, 914);
spinePreviewer.SpineListView = spineListView; spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0; spinePreviewer.TabIndex = 0;
// //
@@ -553,5 +558,6 @@
private ToolStripMenuItem toolStripMenuItem_ExportMov; private ToolStripMenuItem toolStripMenuItem_ExportMov;
private ToolStripMenuItem toolStripMenuItem_ExportMkv; private ToolStripMenuItem toolStripMenuItem_ExportMkv;
private ToolStripMenuItem toolStripMenuItem_ExportWebm; private ToolStripMenuItem toolStripMenuItem_ExportWebm;
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
} }
} }

View File

@@ -24,11 +24,12 @@ namespace SpineViewer
// 在此处将导出菜单需要的类绑定起来 // 在此处将导出菜单需要的类绑定起来
toolStripMenuItem_ExportFrame.Tag = ExportType.Frame; toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence; toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
toolStripMenuItem_ExportGif.Tag = ExportType.GIF; toolStripMenuItem_ExportGif.Tag = ExportType.Gif;
toolStripMenuItem_ExportMkv.Tag = ExportType.MKV; toolStripMenuItem_ExportMkv.Tag = ExportType.Mkv;
toolStripMenuItem_ExportMp4.Tag = ExportType.MP4; toolStripMenuItem_ExportMp4.Tag = ExportType.Mp4;
toolStripMenuItem_ExportMov.Tag = ExportType.MOV; toolStripMenuItem_ExportMov.Tag = ExportType.Mov;
toolStripMenuItem_ExportWebm.Tag = ExportType.WebM; toolStripMenuItem_ExportWebm.Tag = ExportType.Webm;
toolStripMenuItem_ExportCustom.Tag = ExportType.Custom;
// 执行一些初始化工作 // 执行一些初始化工作
try try

View File

@@ -61,6 +61,11 @@ namespace SpineViewer
public ReadOnlyCollection<string> StandardValues { get; private set; } public ReadOnlyCollection<string> StandardValues { get; private set; }
private readonly List<string> standardValues = []; private readonly List<string> standardValues = [];
/// <summary>
/// 是否允许用户自定义
/// </summary>
public bool Customizable { get; set; } = false;
/// <summary> /// <summary>
/// 字符串标准值列表 /// 字符串标准值列表
/// </summary> /// </summary>
@@ -74,7 +79,11 @@ namespace SpineViewer
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true; 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<StandardValuesAttribute>().FirstOrDefault()?.Customizable ?? false;
return !customizable;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{ {