diff --git a/.gitignore b/.gitignore index 8a30d25..2df685e 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +launchSettings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4e46c..5980e05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v0.16.9 + +- 重构 CLI 工具 + ## v0.16.8 - 去除首次的最小化提示弹框 diff --git a/README.en.md b/README.en.md index d224c63..7e98874 100644 --- a/README.en.md +++ b/README.en.md @@ -143,6 +143,7 @@ For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/Sp - [HandyControl](https://github.com/HandyOrg/HandyControl) - [NLog](https://github.com/NLog/NLog) - [SkiaSharp](https://github.com/mono/SkiaSharp) +- [Spectre.Console](https://github.com/spectreconsole/spectre.console) --- diff --git a/README.md b/README.md index 799b175..268500a 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0 - [HandyControl](https://github.com/HandyOrg/HandyControl) - [NLog](https://github.com/NLog/NLog) - [SkiaSharp](https://github.com/mono/SkiaSharp) +- [Spectre.Console](https://github.com/spectreconsole/spectre.console) --- diff --git a/SpineViewer/Extensions/SFMLExtension.cs b/Spine/Exporters/Extension.cs similarity index 69% rename from SpineViewer/Extensions/SFMLExtension.cs rename to Spine/Exporters/Extension.cs index 6882a3a..f6295a0 100644 --- a/SpineViewer/Extensions/SFMLExtension.cs +++ b/Spine/Exporters/Extension.cs @@ -1,18 +1,14 @@ using SFML.Graphics; using SFML.System; -using SkiaSharp; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using System.Windows; -namespace SpineViewer.Extensions +namespace Spine.Exporters { - public static class SFMLExtension + public static class Extension { /// /// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒 @@ -53,31 +49,11 @@ namespace SpineViewer.Extensions public static FloatRect GetBounds(this View self) { return new( - self.Center.X - self.Size.X / 2, - self.Center.Y - self.Size.Y / 2, - self.Size.X, + self.Center.X - self.Size.X / 2, + self.Center.Y - self.Size.Y / 2, + self.Size.X, self.Size.Y ); } - - public static FloatRect ToFloatRect(this Rect self) - { - return new((float)self.X, (float)self.Y, (float)self.Width, (float)self.Height); - } - - public static Vector2f ToVector2f(this Size self) - { - return new((float)self.Width, (float)self.Height); - } - - public static Vector2u ToVector2u(this Size self) - { - return new((uint)self.Width, (uint)self.Height); - } - - public static Vector2i ToVector2i(this Size self) - { - return new((int)self.Width, (int)self.Height); - } } } diff --git a/Spine/Exporters/FFmpegVideoExporter.cs b/Spine/Exporters/FFmpegVideoExporter.cs index ea04666..b52d0ab 100644 --- a/Spine/Exporters/FFmpegVideoExporter.cs +++ b/Spine/Exporters/FFmpegVideoExporter.cs @@ -35,6 +35,33 @@ namespace Spine.Exporters Mov, } + /// + /// Apng 格式预测器算法 + /// + public enum ApngPredMethod + { + None = 0, + Sub = 1, + Up = 2, + Avg = 3, + Paeth = 4, + Mixed = 5, + } + + /// + /// Mov prores_ks 编码器 profile 参数 + /// + public enum MovProfile + { + Auto = -1, + Proxy = 0, + Light = 1, + Standard = 2, + High = 3, + Yuv4444 = 4, + Yuv4444Extreme = 5, + } + /// /// 视频格式 /// @@ -60,10 +87,10 @@ namespace Spine.Exporters private bool _lossless = false; /// - /// [Apng] 预测器算法, 取值范围 0-5, 分别对应 none, sub, up, avg, paeth, mixed + /// [Apng] 预测器算法 /// - public int ApngPred { get => _apngPred; set => _apngPred = Math.Clamp(value, 0, 5); } - private int _apngPred = 5; + public ApngPredMethod PredMethod { get => _predMethod; set => _predMethod = value; } + private ApngPredMethod _predMethod = ApngPredMethod.Mixed; /// /// [Mp4/Webm/Mkv] CRF @@ -72,10 +99,10 @@ namespace Spine.Exporters private int _crf = 23; /// - /// [Mov] prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 + /// [Mov] prores_ks 编码器的配置等级, 越高质量越好, 只有 及以上才有透明通道 /// - public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); } - private int _profile = 5; + public MovProfile Profile { get => _profile; set => _profile = value; } + private MovProfile _profile = MovProfile.Yuv4444Extreme; /// /// 获取的一帧, 结果是预乘的 @@ -142,7 +169,7 @@ namespace Spine.Exporters private void SetApngOptions(FFMpegArgumentOptions options) { - var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {_apngPred}"; + var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {(int)_predMethod}"; options.ForceFormat("apng").WithVideoCodec("apng").ForcePixelFormat("rgba") .WithCustomArgument(customArgs); } @@ -179,7 +206,7 @@ namespace Spine.Exporters var customArgs = "-vf unpremultiply=inplace=1"; options.ForceFormat("mov").WithVideoCodec("prores_ks").ForcePixelFormat("yuva444p10le") .WithFastStart() - .WithCustomArgument($"-profile {_profile}") + .WithCustomArgument($"-profile {(int)_profile}") .WithCustomArgument(customArgs); } } diff --git a/Spine/Exporters/FrameExporter.cs b/Spine/Exporters/FrameExporter.cs index f73f202..7af0b5c 100644 --- a/Spine/Exporters/FrameExporter.cs +++ b/Spine/Exporters/FrameExporter.cs @@ -18,11 +18,27 @@ namespace Spine.Exporters public FrameExporter(uint width = 100, uint height = 100) : base(width, height) { } public FrameExporter(Vector2u resolution) : base(resolution) { } - public SKEncodedImageFormat Format { get => _format; set => _format = value; } + public SKEncodedImageFormat Format + { + get => _format; + set { + switch (value) + { + case SKEncodedImageFormat.Jpeg: + case SKEncodedImageFormat.Png: + case SKEncodedImageFormat.Webp: + _format = value; + break; + default: + _logger.Warn("Omit unsupported exporter format: {0}", value); + break; + } + } + } protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png; public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); } - protected int _quality = 80; + protected int _quality = 100; public override void Export(string output, params SpineObject[] spines) { @@ -33,5 +49,15 @@ namespace Spine.Exporters using var stream = File.OpenWrite(output); data.SaveTo(stream); } + + /// + /// 获取帧图像, 结果是预乘的 + /// + public SKImage ExportMemoryImage(params SpineObject[] spines) + { + using var frame = GetFrame(spines); + var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul); + return SKImage.FromPixelCopy(info, frame.Image.Pixels); + } } } diff --git a/Spine/Exporters/FrameSequenceExporter.cs b/Spine/Exporters/FrameSequenceExporter.cs index 4655178..9fd035a 100644 --- a/Spine/Exporters/FrameSequenceExporter.cs +++ b/Spine/Exporters/FrameSequenceExporter.cs @@ -24,7 +24,7 @@ namespace Spine.Exporters int frameCount = GetFrameCount(); int frameIdx = 0; - _progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}"); + _progressReporter?.Invoke(frameCount, 0, $"[0/{frameCount}] {output}"); // 导出帧序列单独在此处调用进度报告 foreach (var frame in GetFrames(spines)) { if (ct.IsCancellationRequested) @@ -37,7 +37,7 @@ namespace Spine.Exporters var savePath = Path.Combine(output, $"frame_{_fps}_{frameIdx:d6}.png"); var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul); - _progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {savePath}"); + _progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {savePath}"); try { using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels); diff --git a/Spine/Exporters/VideoExporter.cs b/Spine/Exporters/VideoExporter.cs index e1bb672..9cf2a97 100644 --- a/Spine/Exporters/VideoExporter.cs +++ b/Spine/Exporters/VideoExporter.cs @@ -92,7 +92,7 @@ namespace Spine.Exporters } /// - /// 生成帧序列 + /// 生成帧序列, 用于导出帧序列 /// protected IEnumerable GetFrames(SpineObject[] spines) { @@ -121,14 +121,14 @@ namespace Spine.Exporters } /// - /// 生成帧序列, 支持中途取消和进度输出 + /// 生成帧序列, 支持中途取消和进度输出, 用于动图视频等单个文件输出 /// protected IEnumerable GetFrames(SpineObject[] spines, string output, CancellationToken ct) { int frameCount = GetFrameCount(); int frameIdx = 0; - _progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}"); + _progressReporter?.Invoke(frameCount, 0, $"[0/{frameCount}] {output}"); foreach (var frame in GetFrames(spines)) { if (ct.IsCancellationRequested) @@ -138,7 +138,7 @@ namespace Spine.Exporters break; } - _progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}"); + _progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {output}"); yield return frame; frameIdx++; } diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj index 8bcc50c..1e0cc28 100644 --- a/Spine/Spine.csproj +++ b/Spine/Spine.csproj @@ -7,7 +7,7 @@ net8.0-windows $(SolutionDir)out false - 0.16.8 + 0.16.9 diff --git a/SpineViewer/App.xaml.cs b/SpineViewer/App.xaml.cs index 359deba..af068ba 100644 --- a/SpineViewer/App.xaml.cs +++ b/SpineViewer/App.xaml.cs @@ -84,12 +84,13 @@ namespace SpineViewer var fileTarget = new NLog.Targets.FileTarget("fileTarget") { Encoding = System.Text.Encoding.UTF8, + Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}", + AutoFlush = true, FileName = "${basedir}/logs/app.log", ArchiveFileName = "${basedir}/logs/app.{#}.log", ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling, ArchiveAboveSize = 1048576, MaxArchiveFiles = 5, - Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}", ConcurrentWrites = true, KeepFileOpen = false, }; diff --git a/SpineViewer/Extensions/WpfExtension.cs b/SpineViewer/Extensions/WpfExtension.cs index d5cd487..9ce86e0 100644 --- a/SpineViewer/Extensions/WpfExtension.cs +++ b/SpineViewer/Extensions/WpfExtension.cs @@ -1,19 +1,41 @@ -using SkiaSharp; +using SFML.Graphics; +using SFML.System; +using SkiaSharp; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using System.Windows.Media; using System.Windows; +using System.Windows.Media; using System.Windows.Media.Imaging; -using System.Runtime.InteropServices; namespace SpineViewer.Extensions { public static class WpfExtension { + public static FloatRect ToFloatRect(this Rect self) + { + return new((float)self.X, (float)self.Y, (float)self.Width, (float)self.Height); + } + + public static Vector2f ToVector2f(this Size self) + { + return new((float)self.Width, (float)self.Height); + } + + public static Vector2u ToVector2u(this Size self) + { + return new((uint)self.Width, (uint)self.Height); + } + + public static Vector2i ToVector2i(this Size self) + { + return new((int)self.Width, (int)self.Height); + } + /// /// 从本地 WebP 文件读取,并保留透明度,返回一个可以直接用于 WPF Image.Source 的 BitmapSource。 /// diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml index 418e0c5..e4bdbfd 100644 --- a/SpineViewer/Resources/Strings/en.xaml +++ b/SpineViewer/Resources/Strings/en.xaml @@ -206,12 +206,12 @@ [Webp] Quality parameter, range 0-100, higher value means better quality Lossless Compression [Webp] Lossless compression, quality parameter will be ignored - Predictor Method - [Apng] Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed. It affects encoding time and file size. + Predictor Method + [Apng] Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed. It affects encoding time and file size. CRF Parameter [Mp4/Webm/Mkv] CRF parameter, range 0-63, lower value means higher quality Profile Parameter - [Mov] Profile parameter, integer between -1 and 5, -1 means automatic, higher values indicate higher quality, Alpha channel encoding is only available when value is 4 or higher + [Mov] Profile parameter, an integer between -1 and 5, corresponding to: auto, proxy, lt, standard, hq, 4444, and 4444xq. Alpha channel encoding is available only when the value is 4 or higher. Export Format FFmpeg export format (equivalent to "-f"), e.g. "mp4", "webm" diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml index 766eb13..16559c7 100644 --- a/SpineViewer/Resources/Strings/ja.xaml +++ b/SpineViewer/Resources/Strings/ja.xaml @@ -206,12 +206,12 @@ [Webp] 品質パラメータ、範囲は0-100。値が高いほど品質が良い 無損失圧縮 [Webp] 無損失圧縮、品質パラメータは無視されます - 予測器方式 - [Apng] Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。 エンコード時間とファイルサイズに影響します。 + 予測器方式 + [Apng] Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。 エンコード時間とファイルサイズに影響します。 CRF パラメータ [Mp4/Webm/Mkv] CRF パラメータ、範囲0-63。値が小さいほど品質が高い プロファイルパラメータ - [Mov] プロファイルパラメータ、-1から5の整数、 -1は自動、値が大きいほど品質が高い、 値が4以上の場合のみアルファチャンネルをエンコード可能 + [Mov] Profile パラメータ。値は -1 ~ 5 の整数で、 それぞれ auto、proxy、lt、standard、hq、4444、4444xq に対応します。 値が 4 以上の場合のみアルファチャンネルのエンコードが可能です。 エクスポートフォーマット FFmpegエクスポートフォーマット。パラメーター“-f”に相当します。例: “mp4”、“webm” diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml index 95d462f..9fc695c 100644 --- a/SpineViewer/Resources/Strings/zh.xaml +++ b/SpineViewer/Resources/Strings/zh.xaml @@ -206,12 +206,12 @@ [Webp] 质量参数,取值范围 0-100,越高质量越好 无损压缩 [Webp] 无损压缩,会忽略质量参数 - 预测器方法 - [Apng] Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略, 影响编码时间和文件大小 + 预测器方法 + [Apng] Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略, 影响编码时间和文件大小 CRF 参数 [Mp4/Webm/Mkv] CRF 参数,取值范围 0-63,越小质量越高 Profile 参数 - [Mov] Profile 参数,取值集合为 -1 到 5 之间的整数, -1 表示自动,0-5 取值越高质量越高, 仅在取值大于等于 4 时可以编码透明度通道 + [Mov] Profile 参数,取值范围为 -1 到 5 之间的整数, 分别对应 auto、proxy、lt、standard、hq、4444、4444xq 几种配置, 仅在取值大于等于 4 时可以编码透明度通道 导出格式 FFmpeg 导出格式,等价于参数 “-f”,例如 “mp4”、“webm” diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj index c839c81..f61cea0 100644 --- a/SpineViewer/SpineViewer.csproj +++ b/SpineViewer/SpineViewer.csproj @@ -7,7 +7,7 @@ net8.0-windows $(SolutionDir)out false - 0.16.8 + 0.16.9 WinExe true diff --git a/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs index 474c03b..446639e 100644 --- a/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs +++ b/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs @@ -19,6 +19,8 @@ namespace SpineViewer.ViewModels.Exporters public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain) { public static ImmutableArray VideoFormatOptions { get; } = Enum.GetValues().ToImmutableArray(); + public static ImmutableArray ApngPredMethodOptions { get; } = Enum.GetValues().ToImmutableArray(); + public static ImmutableArray MovProfileOptions { get; } = Enum.GetValues().ToImmutableArray(); public FFmpegVideoExporter.VideoFormat Format { @@ -57,8 +59,8 @@ namespace SpineViewer.ViewModels.Exporters public bool EnableParamLossless => _format == FFmpegVideoExporter.VideoFormat.Webp; - public int ApngPred { get => _apngPred; set => SetProperty(ref _apngPred, Math.Clamp(value, 0, 5)); } - protected int _apngPred = 5; + public FFmpegVideoExporter.ApngPredMethod PredMethod { get => _predMethod; set => SetProperty(ref _predMethod, value); } + protected FFmpegVideoExporter.ApngPredMethod _predMethod = FFmpegVideoExporter.ApngPredMethod.Mixed; public bool EnableParamApngPred => _format == FFmpegVideoExporter.VideoFormat.Apng; @@ -71,8 +73,8 @@ namespace SpineViewer.ViewModels.Exporters _format == FFmpegVideoExporter.VideoFormat.Webm || _format == FFmpegVideoExporter.VideoFormat.Mkv; - public int Profile { get => _profile; set => SetProperty(ref _profile, Math.Clamp(value, -1, 5)); } - protected int _profile = 5; + public FFmpegVideoExporter.MovProfile Profile { get => _profile; set => SetProperty(ref _profile, value); } + protected FFmpegVideoExporter.MovProfile _profile = FFmpegVideoExporter.MovProfile.Yuv4444Extreme; public bool EnableParamProfile => _format == FFmpegVideoExporter.VideoFormat.Mov; @@ -102,7 +104,7 @@ namespace SpineViewer.ViewModels.Exporters Loop = _loop, Quality = _quality, Lossless = _lossless, - ApngPred = _apngPred, + PredMethod = _predMethod, Crf = _crf, Profile = _profile, }; diff --git a/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs index 5a27099..839805c 100644 --- a/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs +++ b/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs @@ -20,13 +20,17 @@ namespace SpineViewer.ViewModels.Exporters { public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain) { - public static ImmutableArray FrameFormatOptions { get; } = Enum.GetValues().ToImmutableArray(); + public static ImmutableArray FrameFormatOptions { get; } = [ + SKEncodedImageFormat.Png, + SKEncodedImageFormat.Webp, + SKEncodedImageFormat.Jpeg, + ]; public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); } protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png; public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); } - protected int _quality = 80; + protected int _quality = 100; private string FormatSuffix { diff --git a/SpineViewer/Views/ExporterDialogs/FFmpegVideoExporterDialog.xaml b/SpineViewer/Views/ExporterDialogs/FFmpegVideoExporterDialog.xaml index 1d60ef6..91e3efa 100644 --- a/SpineViewer/Views/ExporterDialogs/FFmpegVideoExporterDialog.xaml +++ b/SpineViewer/Views/ExporterDialogs/FFmpegVideoExporterDialog.xaml @@ -247,8 +247,11 @@ -