From 99d81c43297790076bc52ef8c6ca4d3e5ba6e067 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Wed, 26 Mar 2025 13:10:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0GIF=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExportArgs/GifExportArgs.cs | 51 +++++++++++++ .../Implementations/Exporter/GifExporter.cs | 76 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs create mode 100644 SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs new file mode 100644 index 0000000..ca7785f --- /dev/null +++ b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Exporter.Implementations.ExportArgs +{ + /// + /// GIF 导出参数 + /// + [ExportImplementation(ExportType.GIF)] + public class GifExportArgs : VideoExportArgs + { + public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) + { + // GIF 的帧率不能太高, 超过 50 帧反而会变慢 + FPS = 25; + } + + /// + /// 调色板最大颜色数量 + /// + [Category("GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")] + public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); } + private uint maxColors = 256; + + /// + /// 透明度阈值 + /// + [Category("GIF 参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")] + public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; } + private byte alphaThreshold = 128; + + /// + /// 获取构造好的 FFMpegCore 自定义参数 + /// + [Browsable(false)] + public string FFMpegCoreCustomArguments + { + 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}\""; + } + } + } +} diff --git a/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs b/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs new file mode 100644 index 0000000..3053675 --- /dev/null +++ b/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs @@ -0,0 +1,76 @@ +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; + +namespace SpineViewer.Exporter.Implementations.Exporter +{ + /// + /// GIF 动图导出器 + /// + [ExportImplementation(ExportType.GIF)] + public class GifExporter : VideoExporter + { + public GifExporter(GifExportArgs exportArgs) : base(exportArgs) { } + + protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) + { + var args = (GifExportArgs)ExportArgs; + + // 导出单个时必定提供输出文件夹 + 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 + { + FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(savePath, true, options => options + .ForceFormat("gif") + .WithCustomArgument(args.FFMpegCoreCustomArguments)) + .ProcessSynchronously(); + } + catch (Exception ex) + { + Program.Logger.Error(ex.ToString()); + Program.Logger.Error("Failed to export gif {}", savePath); + } + } + + protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) + { + var args = (GifExportArgs)ExportArgs; + foreach (var spine in spinesToRender) + { + if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 + + // 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下 + 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 }; + try + { + FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(savePath, true, options => options + .ForceFormat("gif") + .WithCustomArgument(args.FFMpegCoreCustomArguments)) + .ProcessSynchronously(); + } + catch (Exception ex) + { + Program.Logger.Error(ex.ToString()); + Program.Logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath); + } + } + } + } +}