From a16f2f096d35557255fc49ba9e49e2b4b304a0ff Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 24 Mar 2025 13:47:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A2=84=E8=A7=88=E5=9B=BE?= =?UTF-8?q?=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialogs/ExportPreviewDialog.Designer.cs | 105 ++++--------- SpineViewer/Dialogs/ExportPreviewDialog.cs | 143 ++++++++++++++---- SpineViewer/ExportHelper.cs | 64 ++++++++ SpineViewer/MainForm.cs | 31 ++-- 4 files changed, 221 insertions(+), 122 deletions(-) create mode 100644 SpineViewer/ExportHelper.cs diff --git a/SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs b/SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs index 68d8119..0629ded 100644 --- a/SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs +++ b/SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs @@ -31,23 +31,18 @@ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPreviewDialog)); panel1 = new Panel(); tableLayoutPanel1 = new TableLayoutPanel(); + propertyGrid = new PropertyGrid(); label4 = new Label(); label1 = new Label(); - label2 = new Label(); - label3 = new Label(); textBox_OutputDir = new TextBox(); button_SelectOutputDir = new Button(); tableLayoutPanel2 = new TableLayoutPanel(); button_Ok = new Button(); button_Cancel = new Button(); - numericUpDown_Width = new NumericUpDown(); - numericUpDown_Height = new NumericUpDown(); folderBrowserDialog = new FolderBrowserDialog(); panel1.SuspendLayout(); tableLayoutPanel1.SuspendLayout(); tableLayoutPanel2.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)numericUpDown_Width).BeginInit(); - ((System.ComponentModel.ISupportInitialize)numericUpDown_Height).BeginInit(); SuspendLayout(); // // panel1 @@ -57,7 +52,7 @@ panel1.Location = new Point(0, 0); panel1.Name = "panel1"; panel1.Padding = new Padding(50, 15, 50, 10); - panel1.Size = new Size(919, 276); + panel1.Size = new Size(914, 482); panel1.TabIndex = 2; // // tableLayoutPanel1 @@ -68,27 +63,34 @@ tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel1.Controls.Add(propertyGrid, 0, 2); tableLayoutPanel1.Controls.Add(label4, 0, 0); tableLayoutPanel1.Controls.Add(label1, 0, 1); - tableLayoutPanel1.Controls.Add(label2, 0, 2); - tableLayoutPanel1.Controls.Add(label3, 0, 3); tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1); tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1); - tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 4); - tableLayoutPanel1.Controls.Add(numericUpDown_Width, 1, 2); - tableLayoutPanel1.Controls.Add(numericUpDown_Height, 1, 3); + tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 3); tableLayoutPanel1.Dock = DockStyle.Fill; tableLayoutPanel1.Location = new Point(50, 15); tableLayoutPanel1.Name = "tableLayoutPanel1"; - tableLayoutPanel1.RowCount = 5; + tableLayoutPanel1.RowCount = 4; tableLayoutPanel1.RowStyles.Add(new RowStyle()); tableLayoutPanel1.RowStyles.Add(new RowStyle()); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); tableLayoutPanel1.RowStyles.Add(new RowStyle()); - tableLayoutPanel1.RowStyles.Add(new RowStyle()); - tableLayoutPanel1.RowStyles.Add(new RowStyle()); - tableLayoutPanel1.Size = new Size(819, 251); + tableLayoutPanel1.Size = new Size(814, 457); tableLayoutPanel1.TabIndex = 0; // + // propertyGrid + // + tableLayoutPanel1.SetColumnSpan(propertyGrid, 4); + propertyGrid.Dock = DockStyle.Fill; + propertyGrid.HelpVisible = false; + propertyGrid.Location = new Point(3, 97); + propertyGrid.Name = "propertyGrid"; + propertyGrid.Size = new Size(808, 284); + propertyGrid.TabIndex = 1; + propertyGrid.ToolbarVisible = false; + // // label4 // label4.AutoSize = true; @@ -97,9 +99,9 @@ label4.Location = new Point(15, 15); label4.Margin = new Padding(15); label4.Name = "label4"; - label4.Size = new Size(789, 24); + label4.Size = new Size(784, 24); label4.TabIndex = 11; - label4.Text = "说明:导出的文件名与骨骼文件名相同"; + label4.Text = "说明:输出文件夹为可选项,留空则将预览图输出到每个骨骼文件所在目录"; label4.TextAlign = ContentAlignment.MiddleCenter; // // label1 @@ -112,40 +114,20 @@ label1.TabIndex = 0; label1.Text = "输出文件夹:"; // - // label2 - // - label2.Anchor = AnchorStyles.Right; - label2.AutoSize = true; - label2.Location = new Point(75, 100); - label2.Name = "label2"; - label2.Size = new Size(32, 24); - label2.TabIndex = 1; - label2.Text = "宽:"; - // - // label3 - // - label3.Anchor = AnchorStyles.Right; - label3.AutoSize = true; - label3.Location = new Point(75, 136); - label3.Name = "label3"; - label3.Size = new Size(32, 24); - label3.TabIndex = 2; - label3.Text = "高:"; - // // textBox_OutputDir // tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2); textBox_OutputDir.Dock = DockStyle.Fill; textBox_OutputDir.Location = new Point(113, 57); textBox_OutputDir.Name = "textBox_OutputDir"; - textBox_OutputDir.Size = new Size(664, 30); + textBox_OutputDir.Size = new Size(660, 30); textBox_OutputDir.TabIndex = 3; // // button_SelectOutputDir // button_SelectOutputDir.AutoSize = true; button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink; - button_SelectOutputDir.Location = new Point(783, 57); + button_SelectOutputDir.Location = new Point(779, 57); button_SelectOutputDir.Name = "button_SelectOutputDir"; button_SelectOutputDir.Size = new Size(32, 34); button_SelectOutputDir.TabIndex = 5; @@ -164,17 +146,18 @@ tableLayoutPanel2.Controls.Add(button_Ok, 0, 0); tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0); tableLayoutPanel2.Dock = DockStyle.Bottom; - tableLayoutPanel2.Location = new Point(3, 208); + tableLayoutPanel2.Location = new Point(3, 414); + tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3); tableLayoutPanel2.Name = "tableLayoutPanel2"; tableLayoutPanel2.RowCount = 1; tableLayoutPanel2.RowStyles.Add(new RowStyle()); - tableLayoutPanel2.Size = new Size(813, 40); + tableLayoutPanel2.Size = new Size(808, 40); tableLayoutPanel2.TabIndex = 10; // // button_Ok // button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; - button_Ok.Location = new Point(264, 3); + button_Ok.Location = new Point(262, 3); button_Ok.Margin = new Padding(3, 3, 30, 3); button_Ok.Name = "button_Ok"; button_Ok.Size = new Size(112, 34); @@ -186,7 +169,7 @@ // button_Cancel // button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; - button_Cancel.Location = new Point(436, 3); + button_Cancel.Location = new Point(434, 3); button_Cancel.Margin = new Padding(30, 3, 3, 3); button_Cancel.Name = "button_Cancel"; button_Cancel.Size = new Size(112, 34); @@ -195,30 +178,6 @@ button_Cancel.UseVisualStyleBackColor = true; button_Cancel.Click += button_Cancel_Click; // - // numericUpDown_Width - // - numericUpDown_Width.Anchor = AnchorStyles.Left; - numericUpDown_Width.Location = new Point(113, 97); - numericUpDown_Width.Maximum = new decimal(new int[] { 4096, 0, 0, 0 }); - numericUpDown_Width.Minimum = new decimal(new int[] { 32, 0, 0, 0 }); - numericUpDown_Width.Name = "numericUpDown_Width"; - numericUpDown_Width.Size = new Size(180, 30); - numericUpDown_Width.TabIndex = 12; - numericUpDown_Width.TextAlign = HorizontalAlignment.Right; - numericUpDown_Width.Value = new decimal(new int[] { 256, 0, 0, 0 }); - // - // numericUpDown_Height - // - numericUpDown_Height.Anchor = AnchorStyles.Left; - numericUpDown_Height.Location = new Point(113, 133); - numericUpDown_Height.Maximum = new decimal(new int[] { 4096, 0, 0, 0 }); - numericUpDown_Height.Minimum = new decimal(new int[] { 32, 0, 0, 0 }); - numericUpDown_Height.Name = "numericUpDown_Height"; - numericUpDown_Height.Size = new Size(180, 30); - numericUpDown_Height.TabIndex = 13; - numericUpDown_Height.TextAlign = HorizontalAlignment.Right; - numericUpDown_Height.Value = new decimal(new int[] { 256, 0, 0, 0 }); - // // folderBrowserDialog // folderBrowserDialog.AddToRecent = false; @@ -229,7 +188,7 @@ AutoScaleDimensions = new SizeF(11F, 24F); AutoScaleMode = AutoScaleMode.Font; CancelButton = button_Cancel; - ClientSize = new Size(919, 276); + ClientSize = new Size(914, 482); Controls.Add(panel1); FormBorderStyle = FormBorderStyle.FixedDialog; Icon = (Icon)resources.GetObject("$this.Icon"); @@ -239,14 +198,11 @@ ShowInTaskbar = false; StartPosition = FormStartPosition.CenterScreen; Text = "导出预览图"; - Load += ExportPreviewDialog_Load; panel1.ResumeLayout(false); panel1.PerformLayout(); tableLayoutPanel1.ResumeLayout(false); tableLayoutPanel1.PerformLayout(); tableLayoutPanel2.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)numericUpDown_Width).EndInit(); - ((System.ComponentModel.ISupportInitialize)numericUpDown_Height).EndInit(); ResumeLayout(false); } @@ -256,15 +212,12 @@ private TableLayoutPanel tableLayoutPanel1; private Label label4; private Label label1; - private Label label2; - private Label label3; private TextBox textBox_OutputDir; private Button button_SelectOutputDir; private TableLayoutPanel tableLayoutPanel2; private Button button_Ok; private Button button_Cancel; - private NumericUpDown numericUpDown_Width; - private NumericUpDown numericUpDown_Height; private FolderBrowserDialog folderBrowserDialog; + private PropertyGrid propertyGrid; } } \ No newline at end of file diff --git a/SpineViewer/Dialogs/ExportPreviewDialog.cs b/SpineViewer/Dialogs/ExportPreviewDialog.cs index 0353e59..b69143b 100644 --- a/SpineViewer/Dialogs/ExportPreviewDialog.cs +++ b/SpineViewer/Dialogs/ExportPreviewDialog.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; +using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -12,59 +13,58 @@ namespace SpineViewer.Dialogs { public partial class ExportPreviewDialog: Form { - // TODO: 用单独的结果包装类 - public string OutputDir { get; private set; } - public uint PreviewWidth { get; private set; } - public uint PreviewHeight { get; private set; } + /// + /// 对话框结果 + /// + public readonly ExportPreviewDialogResult Result = new(); public ExportPreviewDialog() { InitializeComponent(); - } - - private void ExportPreviewDialog_Load(object sender, EventArgs e) - { - button_SelectOutputDir_Click(sender, e); + propertyGrid.SelectedObject = Result; } private void button_SelectOutputDir_Click(object sender, EventArgs e) { folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text; - if (folderBrowserDialog.ShowDialog() == DialogResult.OK) - { - textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath); - } + if (folderBrowserDialog.ShowDialog() != DialogResult.OK) + return; + textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath); } private void button_Ok_Click(object sender, EventArgs e) { var outputDir = textBox_OutputDir.Text; - if (File.Exists(outputDir)) + if (string.IsNullOrEmpty(outputDir)) { - MessageBox.Info("输出文件夹无效"); - return; + Result.OutputDir = null; } - - if (!Directory.Exists(outputDir)) + else { - if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK) - return; - - try + if (File.Exists(outputDir)) { - Directory.CreateDirectory(outputDir); - } - catch (Exception ex) - { - Program.Logger.Error(ex.ToString()); - MessageBox.Error(ex.ToString(), "文件夹创建失败"); + MessageBox.Info("输出文件夹无效"); return; } - } - OutputDir = Path.GetFullPath(outputDir); - PreviewWidth = (uint)numericUpDown_Width.Value; - PreviewHeight = (uint)numericUpDown_Height.Value; + if (!Directory.Exists(outputDir)) + { + if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK) + return; + + try + { + Directory.CreateDirectory(outputDir); + } + catch (Exception ex) + { + Program.Logger.Error(ex.ToString()); + MessageBox.Error(ex.ToString(), "文件夹创建失败"); + return; + } + } + Result.OutputDir = Path.GetFullPath(outputDir); + } DialogResult = DialogResult.OK; } @@ -74,4 +74,83 @@ namespace SpineViewer.Dialogs DialogResult = DialogResult.Cancel; } } + + public class ExportPreviewDialogResult + { + /// + /// 输出路径 + /// + [Browsable(false)] + public string? OutputDir { get; set; } = null; + + /// + /// 预览图格式 + /// + [TypeConverter(typeof(ImageFormatConverter))] + [Category("导出参数"), DisplayName("预览图格式")] + public ImageFormat ImageFormat + { + get => imageFormat; + set + { + if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp; + imageFormat = value; + } + } + private ImageFormat imageFormat = ImageFormat.Png; + + /// + /// 预览图分辨率 + /// + [TypeConverter(typeof(SizeConverter))] + [Category("导出参数"), DisplayName("分辨率")] + public Size Resolution + { + get => resolution; + set + { + if (value.Width <= 0) value.Width = 128; + if (value.Height <= 0) value.Height = 128; + resolution = value; + } + } + private Size resolution = new(512, 512); + + /// + /// 四周填充像素值 + /// + [TypeConverter(typeof(PaddingConverter))] + [Category("导出参数"), DisplayName("四周填充像素值")] + public Padding Padding + { + get => padding; + set + { + if (value.Left <= 0) value.Left = 10; + if (value.Right <= 0) value.Right = 10; + if (value.Top <= 0) value.Top = 10; + if (value.Bottom <= 0) value.Bottom = 10; + padding = value; + } + } + private Padding padding = new(1); + + /// + /// DPI + /// + [TypeConverter(typeof(SizeFConverter))] + [Category("导出参数"), DisplayName("DPI")] + public SizeF DPI + { + get => dpi; + set + { + if (value.Width <= 0) value.Width = 144; + if (value.Height <= 0) value.Height = 144; + dpi = value; + } + } + private SizeF dpi = new(144, 144); + } + } diff --git a/SpineViewer/ExportHelper.cs b/SpineViewer/ExportHelper.cs new file mode 100644 index 0000000..ea517b9 --- /dev/null +++ b/SpineViewer/ExportHelper.cs @@ -0,0 +1,64 @@ +using FFMpegCore.Pipes; +using System; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel; + +namespace SpineViewer +{ + /// + /// SFML.Graphics.Image 帧对象包装类 + /// + public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable + { + public int Width => (int)image.Size.X; + public int Height => (int)image.Size.Y; + public string Format => "rgba"; + public void Serialize(Stream pipe) => pipe.Write(image.Pixels); + public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token); + public void Dispose() => image.Dispose(); + + /// + /// Save the contents of the image to a file + /// + /// Path of the file to save (overwritten if already exist) + /// True if saving was successful + public bool SaveToFile(string filename) => image.SaveToFile(filename); + + /// + /// Save the image to a buffer in memory The format of the image must be specified. + /// The supported image formats are bmp, png, tga and jpg. This function fails if + /// the image is empty, or if the format was invalid. + /// + /// Byte array filled with encoded data + /// Encoding format to use + /// True if saving was successful + public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format); + } + + /// + /// 为帧导出创建的辅助类 + /// + public static class ExportHelper + { + public static Bitmap CopyToBitmap(this SFML.Graphics.Texture tex) + { + using var img = tex.CopyToImage(); + img.SaveToMemory(out var imgBuffer, "bmp"); + using var stream = new MemoryStream(imgBuffer); + return new Bitmap(stream); + } + + public static SFMLImageVideoFrame CopyToFrame(this SFML.Graphics.Texture tex) => new(tex.CopyToImage()); + + public static string GetSuffix(this ImageFormat imageFormat) + { + if (imageFormat == ImageFormat.Icon) return ".ico"; + else if (imageFormat == ImageFormat.Exif) return ".jpg"; + else return $".{imageFormat.ToString().ToLower()}"; + } + } +} diff --git a/SpineViewer/MainForm.cs b/SpineViewer/MainForm.cs index 9832748..c229722 100644 --- a/SpineViewer/MainForm.cs +++ b/SpineViewer/MainForm.cs @@ -106,7 +106,7 @@ namespace SpineViewer var progressDialog = new Dialogs.ProgressDialog(); progressDialog.DoWork += ExportPreview_Work; - progressDialog.RunWorkerAsync(saveDialog); + progressDialog.RunWorkerAsync(saveDialog.Result); progressDialog.ShowDialog(); } @@ -294,17 +294,15 @@ namespace SpineViewer private void ExportPreview_Work(object? sender, DoWorkEventArgs e) { var worker = sender as BackgroundWorker; - var arguments = e.Argument as Dialogs.ExportPreviewDialog; - var outputDir = arguments.OutputDir; - var width = arguments.PreviewWidth; - var height = arguments.PreviewHeight; - // TODO: 增加填充参数 - var paddingL = 1u; - var paddingR = 1u; - var paddingT = 1u; - var paddingB = 1u; + var arguments = e.Argument as Dialogs.ExportPreviewDialogResult; - var tex = new SFML.Graphics.RenderTexture(width, height); + var outputDir = arguments.OutputDir; + var imageFormat = arguments.ImageFormat; + var resolution = arguments.Resolution; + var padding = arguments.Padding; + var dpi = arguments.DPI; + + var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height); int success = 0; int error = 0; @@ -323,18 +321,23 @@ namespace SpineViewer } var spine = spines[i]; + var filename = $"(preview) {spine.Name}{imageFormat.GetSuffix()}"; // 加上 preview 是为了防止覆盖同名的 png 文件 + var savePath = outputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(outputDir, filename); + var tmp = spine.CurrentAnimation; spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION; - tex.SetView(spine.GetInitView(width, height, paddingL, paddingR, paddingT, paddingB)); + tex.SetView(spine.GetInitView(resolution, padding)); tex.Clear(SFML.Graphics.Color.Transparent); tex.Draw(spine); tex.Display(); spine.CurrentAnimation = tmp; + try { - using (var img = tex.Texture.CopyToImage()) + using (var img = new Bitmap(tex.Texture.CopyToBitmap())) { - img.SaveToFile(Path.Combine(outputDir, $"{spine.Name}.png")); + img.SetResolution(dpi.Width, dpi.Height); + img.Save(savePath, imageFormat); } success++; }