Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6a17ec0
Add --warmup option to control physics warmup loops
jayng9663 Oct 19, 2025
d335645
remove unnecessary frame loops
ww-rm Oct 19, 2025
ee7c9e9
Merge pull request #132 from jayng9663/dev/wpf
ww-rm Oct 24, 2025
779500e
修改ApngPred属性名为PredMethod
ww-rm Oct 25, 2025
74538dd
apng和mov格式参数改为枚举量类型
ww-rm Oct 25, 2025
7ca431b
增加System.CommandLine库
ww-rm Oct 25, 2025
c4956b9
重构
ww-rm Oct 26, 2025
aade44c
增加注释
ww-rm Oct 26, 2025
d3b5814
small change
ww-rm Oct 26, 2025
9d9edb8
增加 export 命令
ww-rm Oct 26, 2025
81d9224
增加参数验证
ww-rm Oct 26, 2025
f878530
重构
ww-rm Oct 26, 2025
5eba515
增加 query 命令
ww-rm Oct 26, 2025
e14c54c
调整时间轴处理顺序
ww-rm Oct 26, 2025
cc6d1b6
更新注释
ww-rm Oct 26, 2025
997d553
修复可能的资源泄露
ww-rm Oct 26, 2025
31daed9
移除不受支持的图像格式
ww-rm Oct 26, 2025
c02cec9
修改图像质量默认值为100
ww-rm Oct 26, 2025
aace461
修改方法名
ww-rm Oct 26, 2025
a61bb43
增加preview命令
ww-rm Oct 26, 2025
5498508
移除不受支持的格式
ww-rm Oct 26, 2025
3459f3a
修复进度回调done值错误
ww-rm Oct 26, 2025
df36d46
增加动态进度条
ww-rm Oct 26, 2025
701d1fc
增加日志
ww-rm Oct 26, 2025
29d7e8d
移除依赖库
ww-rm Oct 27, 2025
6171570
增加透明度参数
ww-rm Oct 27, 2025
b54c6a1
update to v0.16.9
ww-rm Oct 27, 2025
7780fbd
update ignore
ww-rm Oct 27, 2025
317ee71
update changelog
ww-rm Oct 27, 2025
49f95dd
add readme
ww-rm Oct 27, 2025
7a29fee
update readme
ww-rm Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,5 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml

launchSettings.json
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## v0.16.9

- 重构 CLI 工具

## v0.16.8

- 去除首次的最小化提示弹框
Expand Down
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
Expand Down Expand Up @@ -53,31 +49,11 @@ public static FloatRect GetCanvasBounds(this FloatRect self, Vector2u resolution
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);
}
}
}
43 changes: 35 additions & 8 deletions Spine/Exporters/FFmpegVideoExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ public enum VideoFormat
Mov,
}

/// <summary>
/// Apng 格式预测器算法
/// </summary>
public enum ApngPredMethod
{
None = 0,
Sub = 1,
Up = 2,
Avg = 3,
Paeth = 4,
Mixed = 5,
}

/// <summary>
/// Mov prores_ks 编码器 profile 参数
/// </summary>
public enum MovProfile
{
Auto = -1,
Proxy = 0,
Light = 1,
Standard = 2,
High = 3,
Yuv4444 = 4,
Yuv4444Extreme = 5,
}

/// <summary>
/// 视频格式
/// </summary>
Expand All @@ -60,10 +87,10 @@ public enum VideoFormat
private bool _lossless = false;

/// <summary>
/// [Apng] 预测器算法, 取值范围 0-5, 分别对应 none, sub, up, avg, paeth, mixed
/// [Apng] 预测器算法
/// </summary>
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;

/// <summary>
/// [Mp4/Webm/Mkv] CRF
Expand All @@ -72,10 +99,10 @@ public enum VideoFormat
private int _crf = 23;

/// <summary>
/// [Mov] prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道
/// [Mov] prores_ks 编码器的配置等级, 越高质量越好, 只有 <see cref="MovProfile.Yuv4444"> 及以上才有透明通道
/// </summary>
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;

/// <summary>
/// 获取的一帧, 结果是预乘的
Expand Down Expand Up @@ -142,7 +169,7 @@ private void SetWebpOptions(FFMpegArgumentOptions options)

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);
}
Expand Down Expand Up @@ -179,7 +206,7 @@ private void SetMovOptions(FFMpegArgumentOptions options)
var customArgs = "-vf unpremultiply=inplace=1";
options.ForceFormat("mov").WithVideoCodec("prores_ks").ForcePixelFormat("yuva444p10le")
.WithFastStart()
.WithCustomArgument($"-profile {_profile}")
.WithCustomArgument($"-profile {(int)_profile}")
.WithCustomArgument(customArgs);
}
}
Expand Down
30 changes: 28 additions & 2 deletions Spine/Exporters/FrameExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,27 @@ public class FrameExporter : BaseExporter
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)
{
Expand All @@ -33,5 +49,15 @@ public override void Export(string output, params SpineObject[] spines)
using var stream = File.OpenWrite(output);
data.SaveTo(stream);
}

/// <summary>
/// 获取帧图像, 结果是预乘的
/// </summary>
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);
}
}
}
4 changes: 2 additions & 2 deletions Spine/Exporters/FrameSequenceExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public override void Export(string output, CancellationToken ct, params SpineObj
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)
Expand All @@ -37,7 +37,7 @@ public override void Export(string output, CancellationToken ct, params SpineObj
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);
Expand Down
8 changes: 4 additions & 4 deletions Spine/Exporters/VideoExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public int GetFrameCount()
}

/// <summary>
/// 生成帧序列
/// 生成帧序列, 用于导出帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines)
{
Expand Down Expand Up @@ -121,14 +121,14 @@ protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines)
}

/// <summary>
/// 生成帧序列, 支持中途取消和进度输出
/// 生成帧序列, 支持中途取消和进度输出, 用于动图视频等单个文件输出
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> 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)
Expand All @@ -138,7 +138,7 @@ protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines, strin
break;
}

_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}");
_progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {output}");
yield return frame;
frameIdx++;
}
Expand Down
2 changes: 1 addition & 1 deletion Spine/Spine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.8</Version>
<Version>0.16.9</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
3 changes: 2 additions & 1 deletion SpineViewer/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@ private static void InitializeLogConfiguration()
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,
};
Expand Down
28 changes: 25 additions & 3 deletions SpineViewer/Extensions/WpfExtension.cs
Original file line number Diff line number Diff line change
@@ -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);
}

/// <summary>
/// 从本地 WebP 文件读取,并保留透明度,返回一个可以直接用于 WPF Image.Source 的 BitmapSource。
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions SpineViewer/Resources/Strings/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]&#x0A;Quality parameter, range 0-100, higher value means better quality</s:String>
<s:String x:Key="Str_LosslessParam">Lossless Compression</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;Lossless compression, quality parameter will be ignored</s:String>
<s:String x:Key="Str_ApngPred">Predictor Method</s:String>
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]&#x0A;Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed.&#x0A;It affects encoding time and file size.</s:String>
<s:String x:Key="Str_PredMethod">Predictor Method</s:String>
<s:String x:Key="Str_PredMethodTooltip" xml:space="preserve">[Apng]&#x0A;Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed.&#x0A;It affects encoding time and file size.</s:String>
<s:String x:Key="Str_CrfParameter">CRF Parameter</s:String>
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]&#x0A;CRF parameter, range 0-63, lower value means higher quality</s:String>
<s:String x:Key="Str_ProfileParameter">Profile Parameter</s:String>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile parameter, integer between -1 and 5,&#x0A;-1 means automatic, higher values indicate higher quality,&#x0A;Alpha channel encoding is only available when value is 4 or higher</s:String>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile parameter, an integer between -1 and 5,&#x0A;corresponding to: auto, proxy, lt, standard, hq, 4444, and 4444xq.&#x0A;Alpha channel encoding is available only when the value is 4 or higher.</s:String>

<s:String x:Key="Str_FFmpegFormat">Export Format</s:String>
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpeg export format (equivalent to "-f"), e.g. "mp4", "webm"</s:String>
Expand Down
6 changes: 3 additions & 3 deletions SpineViewer/Resources/Strings/ja.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]&#x0A;品質パラメータ、範囲は0-100。値が高いほど品質が良い</s:String>
<s:String x:Key="Str_LosslessParam">無損失圧縮</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;無損失圧縮、品質パラメータは無視されます</s:String>
<s:String x:Key="Str_ApngPred">予測器方式</s:String>
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]&#x0A;Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。&#x0A;エンコード時間とファイルサイズに影響します。</s:String>
<s:String x:Key="Str_PredMethod">予測器方式</s:String>
<s:String x:Key="Str_PredMethodTooltip" xml:space="preserve">[Apng]&#x0A;Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。&#x0A;エンコード時間とファイルサイズに影響します。</s:String>
<s:String x:Key="Str_CrfParameter">CRF パラメータ</s:String>
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]&#x0A;CRF パラメータ、範囲0-63。値が小さいほど品質が高い</s:String>
<s:String x:Key="Str_ProfileParameter">プロファイルパラメータ</s:String>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;プロファイルパラメータ、-1から5の整数、&#x0A;-1は自動、値が大きいほど品質が高い、&#x0A;値が4以上の場合のみアルファチャンネルをエンコード可能</s:String>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile パラメータ。値は -1 ~ 5 の整数で、&#x0A;それぞれ auto、proxy、lt、standard、hq、4444、4444xq に対応します。&#x0A;値が 4 以上の場合のみアルファチャンネルのエンコードが可能です。</s:String>

<s:String x:Key="Str_FFmpegFormat">エクスポートフォーマット</s:String>
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpegエクスポートフォーマット。パラメーター“-f”に相当します。例: “mp4”、“webm”</s:String>
Expand Down
6 changes: 3 additions & 3 deletions SpineViewer/Resources/Strings/zh.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]&#x0A;质量参数,取值范围 0-100,越高质量越好</s:String>
<s:String x:Key="Str_LosslessParam">无损压缩</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;无损压缩,会忽略质量参数</s:String>
<s:String x:Key="Str_ApngPred">预测器方法</s:String>
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]&#x0A;Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略,&#x0A;影响编码时间和文件大小</s:String>
<s:String x:Key="Str_PredMethod">预测器方法</s:String>
<s:String x:Key="Str_PredMethodTooltip" xml:space="preserve">[Apng]&#x0A;Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略,&#x0A;影响编码时间和文件大小</s:String>
<s:String x:Key="Str_CrfParameter">CRF 参数</s:String>
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]&#x0A;CRF 参数,取值范围 0-63,越小质量越高</s:String>
<s:String x:Key="Str_ProfileParameter">Profile 参数</s:String>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile 参数,取值集合为 -1 到 5 之间的整数,&#x0A;-1 表示自动,0-5 取值越高质量越高,&#x0A;仅在取值大于等于 4 时可以编码透明度通道</s:String>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile 参数,取值范围为 -1 到 5 之间的整数,&#x0A;分别对应 auto、proxy、lt、standard、hq、4444、4444xq 几种配置,&#x0A;仅在取值大于等于 4 时可以编码透明度通道</s:String>

<s:String x:Key="Str_FFmpegFormat">导出格式</s:String>
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpeg 导出格式,等价于参数 “-f”,例如 “mp4”、“webm”</s:String>
Expand Down
2 changes: 1 addition & 1 deletion SpineViewer/SpineViewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.8</Version>
<Version>0.16.9</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>
Expand Down
Loading
Loading