Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## v0.16.17

- 增加图层混合模式
- 修复可能的导出顺序错误

## v0.16.16

- 增加 PSD 格式导出
Expand Down
8 changes: 4 additions & 4 deletions PsdWriter/PsdWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ public PsdWriter(uint width, uint height)
public uint Width { get; }
public uint Height { get; }

public void AddRgbaLayer(byte[] pixels, string name = null, bool preMultipliedAlpha = false)
public void AddRgbaLayer(byte[] pixels, string name = null, bool preMultipliedAlpha = false, string blendMode = BlendModes.Normal)
{
if (string.IsNullOrWhiteSpace(name))
name = Guid.NewGuid().ToString()[..8];

var layer = new RgbaLayer(name, Width, Height);
var layer = new RgbaLayer(name, Width, Height) { BlendMode = blendMode };
layer.SetRgbaImageData(pixels, preMultipliedAlpha);
_layerAndMaskSection.Layers.Add(layer);
}
Expand All @@ -47,7 +47,7 @@ public void BeginGroup(string name = null)
if (string.IsNullOrWhiteSpace(name))
name = Guid.NewGuid().ToString()[..8];

var layer = new DividerLayer("</Layer group>", DividerLayer.DividerTypes.BoundingSectionDivider);
var layer = new DividerLayer("</Layer group>", DividerTypes.BoundingSectionDivider);
_layerAndMaskSection.Layers.Add(layer);
_groupNames.Push(name);
}
Expand All @@ -58,7 +58,7 @@ public string EndGroup(bool openFolder = false)
throw new IndexOutOfRangeException("No groups");

var name = _groupNames.Pop();
var layer = new DividerLayer(name, openFolder ? DividerLayer.DividerTypes.OpenFolder : DividerLayer.DividerTypes.ClosedFolder);
var layer = new DividerLayer(name, openFolder ? DividerTypes.OpenFolder : DividerTypes.ClosedFolder);
_layerAndMaskSection.Layers.Add(layer);
return name;
}
Expand Down
2 changes: 1 addition & 1 deletion PsdWriter/PsdWriter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.16</Version>
<Version>0.16.17</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
45 changes: 45 additions & 0 deletions PsdWriter/Sections/Layers/BlendModes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PsdWriter.Sections.Layers
{
public static class BlendModes
{
public const string PassThrough = "pass";
public const string Normal = "norm";
public const string Dissolve = "diss";

public const string Darken = "dark";
public const string Multiply = "mul ";
public const string ColorBurn = "idiv";
public const string LinearBurn = "lbrn";
public const string DarkerColor = "dkCl";

public const string Lighten = "lite";
public const string Screen = "scrn";
public const string ColorDodge = "div ";
public const string LinearDodge = "lddg";
public const string LighterColor = "lgCl";

public const string Overlay = "over";
public const string SoftLight = "sLit";
public const string HardLight = "hLit";
public const string VividLight = "vLit";
public const string LinearLight = "lLit";
public const string PinLight = "pLit";
public const string HardMix = "hMix";

public const string Difference = "diff";
public const string Exclusion = "smud";
public const string Subtract = "fsub";
public const string Divide = "fdiv";

public const string Hue = "hue ";
public const string Saturation = "sat ";
public const string Color = "colr";
public const string Luminosity = "lum ";
}
}
8 changes: 0 additions & 8 deletions PsdWriter/Sections/Layers/DividerLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ namespace PsdWriter.Sections.Layers
{
internal class DividerLayer : Layer
{
public static class DividerTypes
{
public const uint Other = 0;
public const uint OpenFolder = 1;
public const uint ClosedFolder = 2;
public const uint BoundingSectionDivider = 3;
}

public DividerLayer(string name, uint dividerType) : base(name, 0, 0)
{
using (var ms = new MemoryStream())
Expand Down
16 changes: 16 additions & 0 deletions PsdWriter/Sections/Layers/DividerTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PsdWriter.Sections.Layers
{
internal static class DividerTypes
{
public const uint Other = 0;
public const uint OpenFolder = 1;
public const uint ClosedFolder = 2;
public const uint BoundingSectionDivider = 3;
}
}
27 changes: 16 additions & 11 deletions PsdWriter/Sections/Layers/Layer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ namespace PsdWriter.Sections.Layers
{
internal abstract class Layer
{
public static class BlendModes
{
public const string Normal = "norm";
public const string PassThrough = "pass";
}

protected readonly string _name;

protected readonly int _top = 0;
Expand All @@ -25,7 +19,7 @@ public static class BlendModes

protected readonly ushort _channels = 4;

protected readonly string _blendMode = BlendModes.Normal;
protected string _blendMode = BlendModes.Normal;

protected readonly byte _opacity = 255;
protected readonly byte _clipping = 0;
Expand Down Expand Up @@ -56,6 +50,17 @@ public Layer(string name, uint width, uint height)
}
}

public string BlendMode
{
get => _blendMode;
set
{
if (string.IsNullOrWhiteSpace(value) || value.Length != 4)
throw new ArgumentException(nameof(value), "Blend mode must be a 4-bytes string");
_blendMode = value;
}
}

/// <summary>
/// 4n bytes (first length byte + valid name bytes + zeros padding)
/// </summary>
Expand Down Expand Up @@ -88,7 +93,7 @@ protected byte[] GetNameUnicodeBytes()
return bytes;
}

public virtual int RecordLength { get => 18 + 6 * _channels + 64 + GetNameBytes().Length + _additionalInfo.Select(x => x.Length).Sum(); }
public virtual int RecordLength { get => 18 + 6 * _channels + 24 + (2 + 2 * _channels) * 4 + GetNameBytes().Length + _additionalInfo.Select(x => x.Length).Sum(); }

public virtual int ChannelDataLength { get => _channelDataA.Length + _channelDataR.Length + _channelDataG.Length + _channelDataB.Length; }

Expand Down Expand Up @@ -131,13 +136,13 @@ public virtual void WriteRecordTo(Stream stream)
stream.WriteByte(0);

// 4 bytes (Length of extra data length, the rest data below)
stream.WriteI32BE(48 + nameBytes.Length + _additionalInfo.Select(x => x.Length).Sum());
stream.WriteI32BE(4 + 4 + (2 + 2 * _channels) * 4 + nameBytes.Length + _additionalInfo.Select(x => x.Length).Sum());

// 4 bytes (Layer mask data)
stream.WriteU32BE(0);

// 44 bytes (Blending ranges)
stream.WriteU32BE(40);
// 4 + (2 + 2 * Channels) * 4 bytes (Blending ranges)
stream.WriteI32BE((2 + 2 * _channels) * 4);
stream.WriteU32Repeats(2 + 2 * _channels, 0x0000FFFF);

// 4n bytes (Layer name padded with zero bytes)
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Spine file viewer & exporter, also a dynamic wallpaper program supporting Spine

## Features

:sparkles: **v0.16.16 Now Supports Exporting PSD with Multiple Layers** :sparkles:
:sparkles: **v0.16.17 Now Supports Exporting PSD with Multiple Layers** :sparkles:

- Supports multiple Spine file versions (`2.1.x; 3.4.x - 4.2.-`)
- List-based multi-skeleton view with rendering order management
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0

## 功能

:sparkles: **v0.16.16 已支持 PSD 多图层格式导出** :sparkles:
:sparkles: **v0.16.17 已支持 PSD 多图层格式导出** :sparkles:

- 支持多版本 spine 文件 (`2.1.x; 3.4.x - 4.2.x`)
- 支持列表式多骨骼查看和渲染层级管理
Expand Down
21 changes: 18 additions & 3 deletions Spine/Exporters/PsdExporter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PsdWriter;
using SFML.System;
using Spine.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -40,15 +41,15 @@ public void Export(string output, CancellationToken ct, params SpineObject[] spi
}

psdWriter.BeginGroup(sp.Name);
foreach (var (name, image) in sp.IterDraw(_renderTexture))
foreach (var (slot, image) in sp.IterDraw(_renderTexture))
{
if (ct.IsCancellationRequested)
{
image.Dispose();
break;
}
_progressReporter?.Invoke(layerCount, layerIdx + 1, $"[{layerIdx + 1}/{layerCount}] {output} <{sp.Name}/{name}>");
psdWriter.AddRgbaLayer(image.Pixels, name, true);
_progressReporter?.Invoke(layerCount, layerIdx + 1, $"[{layerIdx + 1}/{layerCount}] {output} <{sp.Name}/{slot.Name}>");
psdWriter.AddRgbaLayer(image.Pixels, slot.Name, true, ConvertBlendMode(slot.Blend));
image.Dispose();
layerIdx++;
}
Expand All @@ -58,5 +59,19 @@ public void Export(string output, CancellationToken ct, params SpineObject[] spi
_renderTexture.SetActive(false);
psdWriter.WriteTo(output);
}

private string ConvertBlendMode(SFML.Graphics.BlendMode blendMode)
{
if (blendMode == SFMLBlendMode.NormalPma)
return PsdWriter.Sections.Layers.BlendModes.Normal;
else if (blendMode == SFMLBlendMode.AdditivePma)
return PsdWriter.Sections.Layers.BlendModes.LinearDodge;
else if (blendMode == SFMLBlendMode.MultiplyPma)
return PsdWriter.Sections.Layers.BlendModes.Multiply;
else if (blendMode == SFMLBlendMode.ScreenPma)
return PsdWriter.Sections.Layers.BlendModes.Screen;
else
throw new NotImplementedException(blendMode.ToString());
}
}
}
2 changes: 1 addition & 1 deletion Spine/Spine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.16</Version>
<Version>0.16.17</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
22 changes: 9 additions & 13 deletions Spine/SpineObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -869,12 +869,13 @@ public virtual int IterDrawCount
}

/// <summary>
/// 迭代逐 Slot 渲染
/// 迭代逐 Slot 渲染, 每个层的结果是预乘的, 且无混合设置, 需要调用方自行管理层混合方式
/// </summary>
/// <returns>(Name, Image) 二元组, 调用方管理 Image 对象生命周期</returns>
public virtual IEnumerable<(string Name, SFML.Graphics.Image Image)> IterDraw(SFML.Graphics.RenderTexture target)
/// <returns>(Slot, Image) 二元组, 调用方管理 Image 对象生命周期</returns>
public virtual IEnumerable<(ISlot Slot, SFML.Graphics.Image Image)> IterDraw(SFML.Graphics.RenderTexture target)
{
var states = SFML.Graphics.RenderStates.Default;
states.BlendMode = SFMLBlendMode.NormalPma; // 此处固定使用 Normal 混合, 将像素原样渲染
states.Texture = null;
states.Shader = UsePma ? SFMLShader.VertexAlphaPma : SFMLShader.VertexAlpha;

Expand All @@ -900,8 +901,6 @@ public virtual int IterDrawCount
float tintB = _skeleton.B * slot.B;
float tintA = _skeleton.A * slot.A;

SFML.Graphics.Texture texture;

switch (attachment)
{
case IRegionAttachment regionAttachment:
Expand All @@ -916,7 +915,7 @@ public virtual int IterDrawCount
tintA *= regionAttachment.A;

// NOTE: RenderObject 的获取要在 ComputeWorldVertices 发生之后, 否则可能存在某些 Region 尚未被赋值产生 null 引用报错
texture = regionAttachment.RendererObject;
states.Texture = regionAttachment.RendererObject;
break;
case IMeshAttachment meshAttachment:
worldVerticesLength = meshAttachment.ComputeWorldVertices(slot, ref _worldVertices);
Expand All @@ -928,7 +927,7 @@ public virtual int IterDrawCount
tintG *= meshAttachment.G;
tintB *= meshAttachment.B;
tintA *= meshAttachment.A;
texture = meshAttachment.RendererObject;
states.Texture = meshAttachment.RendererObject;
break;
case IClippingAttachment clippingAttachment:
clipping.ClipStart(slot, clippingAttachment);
Expand All @@ -938,9 +937,6 @@ public virtual int IterDrawCount
continue;
}

states.BlendMode = slot.Blend;
states.Texture = texture;

if (clipping.IsClipping)
{
clipping.ClipTriangles(worldVertices, worldVerticesLength, triangles, trianglesLength, uvs);
Expand All @@ -955,8 +951,8 @@ public virtual int IterDrawCount
target.Clear(SFML.Graphics.Color.Transparent);
_triangleVertices.Clear();

var texW = texture.Size.X;
var texH = texture.Size.Y;
var texW = states.Texture.Size.X;
var texH = states.Texture.Size.Y;

SFML.Graphics.Vertex vt = new();
vt.Color.R = (byte)(tintR * 255);
Expand All @@ -979,7 +975,7 @@ public virtual int IterDrawCount
target.Draw(_triangleVertices, states);
target.Display();
var img = target.Texture.CopyToImage();
yield return (slot.Name, img);
yield return (slot, img);
}
clipping.ClipEnd();
}
Expand Down
2 changes: 1 addition & 1 deletion SpineViewer/SpineViewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.16</Version>
<Version>0.16.17</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>
Expand Down
8 changes: 7 additions & 1 deletion SpineViewer/ViewModels/Exporters/BaseExporterViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@ protected void SetAutoResolutionAnimated(VideoExporter exporter, params SpineObj
private void Export_Execute(IList? args)
{
if (!Export_CanExecute(args)) return;
Export(args.Cast<SpineObjectModel>().ToArray());
SpineObjectModel[] selectedItems = args.Cast<SpineObjectModel>().ToArray();
lock (_vmMain.SpineObjects.Lock)
{
// 此处原始顺序是按用户的选择顺序, 而不是列表顺序, 所以需要额外按列表序重取一次
selectedItems = _vmMain.SpineObjects.Intersect(selectedItems).ToArray();
}
Export(selectedItems);
}

private bool Export_CanExecute(IList? args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ private void ListViewSelectionChanged_Execute(IList? args)
if (args is null) return;
lock (_spineObjectModels.Lock)
{
// XXX: 此处顺序是按用户的选择顺序, 而不是列表顺序, 但是对后续操作没有区别
var selectedItems = args.Cast<SpineObjectModel>().ToArray();
foreach (var it in _spineObjectModels.Except(selectedItems)) it.IsSelected = false;
foreach (var it in selectedItems) it.IsSelected = true;
Expand Down
2 changes: 1 addition & 1 deletion SpineViewerCLI/SpineViewerCLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<OutputPath>$(BaseOutputPath)\$(Configuration)\$(PlatformTarget)</OutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.16</Version>
<Version>0.16.17</Version>
<OutputType>Exe</OutputType>
</PropertyGroup>

Expand Down
Loading