From 53c27a27eb83ee9549fd1d8b185e82b379fe44bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=BCper?= Date: Mon, 26 Apr 2021 23:41:05 +0200 Subject: [PATCH] Added GUI tool for TBX Arcs --- 7scarlet.sln | 24 ++ TbxEditor/App.config | 6 + TbxEditor/Form1.Designer.cs | 203 +++++++++++ TbxEditor/Form1.cs | 385 +++++++++++++++++++++ TbxEditor/Form1.resx | 120 +++++++ TbxEditor/Program.cs | 22 ++ TbxEditor/Properties/AssemblyInfo.cs | 36 ++ TbxEditor/Properties/Resources.Designer.cs | 70 ++++ TbxEditor/Properties/Resources.resx | 117 +++++++ TbxEditor/Properties/Settings.Designer.cs | 29 ++ TbxEditor/Properties/Settings.settings | 7 + TbxEditor/TBG.cs | 243 +++++++++++++ TbxEditor/TBX.cs | 92 +++++ TbxEditor/TbxEditor.csproj | 85 +++++ 14 files changed, 1439 insertions(+) create mode 100644 TbxEditor/App.config create mode 100644 TbxEditor/Form1.Designer.cs create mode 100644 TbxEditor/Form1.cs create mode 100644 TbxEditor/Form1.resx create mode 100644 TbxEditor/Program.cs create mode 100644 TbxEditor/Properties/AssemblyInfo.cs create mode 100644 TbxEditor/Properties/Resources.Designer.cs create mode 100644 TbxEditor/Properties/Resources.resx create mode 100644 TbxEditor/Properties/Settings.Designer.cs create mode 100644 TbxEditor/Properties/Settings.settings create mode 100644 TbxEditor/TBG.cs create mode 100644 TbxEditor/TBX.cs create mode 100644 TbxEditor/TbxEditor.csproj diff --git a/7scarlet.sln b/7scarlet.sln index 372eb0f..8565b68 100644 --- a/7scarlet.sln +++ b/7scarlet.sln @@ -11,46 +11,70 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "tbg", "tbg\tbg.vcxproj", "{ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "tbx", "tbx\tbx.vcxproj", "{EDCC7670-6F4C-4388-8157-74A80EB15447}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TbxEditor", "TbxEditor\TbxEditor.csproj", "{DB8E83C0-5AC6-40AD-8247-4B29F340C404}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Debug|Any CPU.ActiveCfg = Debug|Win32 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Debug|x64.ActiveCfg = Debug|x64 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Debug|x64.Build.0 = Debug|x64 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Debug|x86.ActiveCfg = Debug|Win32 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Debug|x86.Build.0 = Debug|Win32 + {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Release|Any CPU.ActiveCfg = Release|Win32 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Release|x64.ActiveCfg = Release|x64 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Release|x64.Build.0 = Release|x64 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Release|x86.ActiveCfg = Release|Win32 {C9E00B0C-4691-4D5D-8317-E5741F78A6D5}.Release|x86.Build.0 = Release|Win32 + {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Debug|Any CPU.ActiveCfg = Debug|Win32 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Debug|x64.ActiveCfg = Debug|x64 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Debug|x64.Build.0 = Debug|x64 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Debug|x86.ActiveCfg = Debug|Win32 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Debug|x86.Build.0 = Debug|Win32 + {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Release|Any CPU.ActiveCfg = Release|Win32 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Release|x64.ActiveCfg = Release|x64 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Release|x64.Build.0 = Release|x64 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Release|x86.ActiveCfg = Release|Win32 {4134257B-EB7C-4FBF-85E5-CD82208F738D}.Release|x86.Build.0 = Release|Win32 + {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Debug|Any CPU.ActiveCfg = Debug|Win32 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Debug|x64.ActiveCfg = Debug|x64 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Debug|x64.Build.0 = Debug|x64 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Debug|x86.ActiveCfg = Debug|Win32 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Debug|x86.Build.0 = Debug|Win32 + {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Release|Any CPU.ActiveCfg = Release|Win32 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Release|x64.ActiveCfg = Release|x64 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Release|x64.Build.0 = Release|x64 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Release|x86.ActiveCfg = Release|Win32 {D7374CCA-5BB3-4EF6-97B4-D0317D87E87C}.Release|x86.Build.0 = Release|Win32 + {EDCC7670-6F4C-4388-8157-74A80EB15447}.Debug|Any CPU.ActiveCfg = Debug|Win32 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Debug|x64.ActiveCfg = Debug|x64 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Debug|x64.Build.0 = Debug|x64 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Debug|x86.ActiveCfg = Debug|Win32 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Debug|x86.Build.0 = Debug|Win32 + {EDCC7670-6F4C-4388-8157-74A80EB15447}.Release|Any CPU.ActiveCfg = Release|Win32 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Release|x64.ActiveCfg = Release|x64 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Release|x64.Build.0 = Release|x64 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Release|x86.ActiveCfg = Release|Win32 {EDCC7670-6F4C-4388-8157-74A80EB15447}.Release|x86.Build.0 = Release|Win32 + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Debug|x64.Build.0 = Debug|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Debug|x86.Build.0 = Debug|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Release|Any CPU.Build.0 = Release|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Release|x64.ActiveCfg = Release|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Release|x64.Build.0 = Release|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Release|x86.ActiveCfg = Release|Any CPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TbxEditor/App.config b/TbxEditor/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/TbxEditor/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TbxEditor/Form1.Designer.cs b/TbxEditor/Form1.Designer.cs new file mode 100644 index 0000000..9359340 --- /dev/null +++ b/TbxEditor/Form1.Designer.cs @@ -0,0 +1,203 @@ + +namespace TbxEditor +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.listView = new System.Windows.Forms.ListView(); + this.btnOpen = new System.Windows.Forms.Button(); + this.btnImport = new System.Windows.Forms.Button(); + this.btnSave = new System.Windows.Forms.Button(); + this.pictureBox1 = new System.Windows.Forms.PictureBox(); + this.btnNew = new System.Windows.Forms.Button(); + this.btnExport = new System.Windows.Forms.Button(); + this.btnRemove = new System.Windows.Forms.Button(); + this.btnUp = new System.Windows.Forms.Button(); + this.btnDown = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); + this.SuspendLayout(); + // + // listView + // + this.listView.AllowDrop = true; + this.listView.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.listView.GridLines = true; + this.listView.HideSelection = false; + this.listView.LabelWrap = false; + this.listView.Location = new System.Drawing.Point(12, 51); + this.listView.Name = "listView"; + this.listView.Size = new System.Drawing.Size(156, 420); + this.listView.TabIndex = 0; + this.listView.UseCompatibleStateImageBehavior = false; + this.listView.ItemDrag += new System.Windows.Forms.ItemDragEventHandler(this.listView_ItemDrag); + this.listView.SelectedIndexChanged += new System.EventHandler(this.listView_SelectedIndexChanged); + this.listView.DragDrop += new System.Windows.Forms.DragEventHandler(this.listView_DragDrop); + this.listView.DragOver += new System.Windows.Forms.DragEventHandler(this.listView_DragOver); + // + // btnOpen + // + this.btnOpen.Location = new System.Drawing.Point(93, 12); + this.btnOpen.Name = "btnOpen"; + this.btnOpen.Size = new System.Drawing.Size(75, 23); + this.btnOpen.TabIndex = 1; + this.btnOpen.Text = "Open"; + this.btnOpen.UseVisualStyleBackColor = true; + this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click); + // + // btnImport + // + this.btnImport.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnImport.Location = new System.Drawing.Point(473, 12); + this.btnImport.Name = "btnImport"; + this.btnImport.Size = new System.Drawing.Size(75, 23); + this.btnImport.TabIndex = 2; + this.btnImport.Text = "Import"; + this.btnImport.UseVisualStyleBackColor = true; + this.btnImport.Click += new System.EventHandler(this.btnImport_Click); + // + // btnSave + // + this.btnSave.Location = new System.Drawing.Point(174, 12); + this.btnSave.Name = "btnSave"; + this.btnSave.Size = new System.Drawing.Size(75, 23); + this.btnSave.TabIndex = 2; + this.btnSave.Text = "Save"; + this.btnSave.UseVisualStyleBackColor = true; + this.btnSave.Click += new System.EventHandler(this.btnSave_Click); + // + // pictureBox1 + // + this.pictureBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.pictureBox1.Location = new System.Drawing.Point(174, 51); + this.pictureBox1.Name = "pictureBox1"; + this.pictureBox1.Size = new System.Drawing.Size(536, 449); + this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pictureBox1.TabIndex = 3; + this.pictureBox1.TabStop = false; + // + // btnNew + // + this.btnNew.Location = new System.Drawing.Point(12, 12); + this.btnNew.Name = "btnNew"; + this.btnNew.Size = new System.Drawing.Size(75, 23); + this.btnNew.TabIndex = 2; + this.btnNew.Text = "New"; + this.btnNew.UseVisualStyleBackColor = true; + this.btnNew.Click += new System.EventHandler(this.btnNew_Click); + // + // btnExport + // + this.btnExport.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnExport.Location = new System.Drawing.Point(554, 12); + this.btnExport.Name = "btnExport"; + this.btnExport.Size = new System.Drawing.Size(75, 23); + this.btnExport.TabIndex = 2; + this.btnExport.Text = "Export"; + this.btnExport.UseVisualStyleBackColor = true; + this.btnExport.Click += new System.EventHandler(this.btnExport_Click); + // + // btnRemove + // + this.btnRemove.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnRemove.Enabled = false; + this.btnRemove.Location = new System.Drawing.Point(635, 12); + this.btnRemove.Name = "btnRemove"; + this.btnRemove.Size = new System.Drawing.Size(75, 23); + this.btnRemove.TabIndex = 4; + this.btnRemove.Text = "Remove"; + this.btnRemove.UseVisualStyleBackColor = true; + this.btnRemove.Click += new System.EventHandler(this.btnRemove_Click); + // + // btnUp + // + this.btnUp.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.btnUp.Enabled = false; + this.btnUp.Location = new System.Drawing.Point(12, 477); + this.btnUp.Name = "btnUp"; + this.btnUp.Size = new System.Drawing.Size(75, 23); + this.btnUp.TabIndex = 2; + this.btnUp.Text = "Move Up"; + this.btnUp.UseVisualStyleBackColor = true; + this.btnUp.Click += new System.EventHandler(this.btnUp_Click); + // + // btnDown + // + this.btnDown.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.btnDown.Enabled = false; + this.btnDown.Location = new System.Drawing.Point(93, 477); + this.btnDown.Name = "btnDown"; + this.btnDown.Size = new System.Drawing.Size(75, 23); + this.btnDown.TabIndex = 2; + this.btnDown.Text = "Move Down"; + this.btnDown.UseVisualStyleBackColor = true; + this.btnDown.Click += new System.EventHandler(this.btnDown_Click); + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(722, 512); + this.Controls.Add(this.btnRemove); + this.Controls.Add(this.pictureBox1); + this.Controls.Add(this.btnDown); + this.Controls.Add(this.btnUp); + this.Controls.Add(this.btnNew); + this.Controls.Add(this.btnSave); + this.Controls.Add(this.btnExport); + this.Controls.Add(this.btnImport); + this.Controls.Add(this.btnOpen); + this.Controls.Add(this.listView); + this.KeyPreview = true; + this.MinimumSize = new System.Drawing.Size(520, 150); + this.Name = "Form1"; + this.Text = "TbxEditor"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); + this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.Form1_KeyDown); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.ListView listView; + private System.Windows.Forms.Button btnOpen; + private System.Windows.Forms.Button btnImport; + private System.Windows.Forms.Button btnSave; + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.Button btnNew; + private System.Windows.Forms.Button btnExport; + private System.Windows.Forms.Button btnRemove; + private System.Windows.Forms.Button btnUp; + private System.Windows.Forms.Button btnDown; + } +} + diff --git a/TbxEditor/Form1.cs b/TbxEditor/Form1.cs new file mode 100644 index 0000000..351eb7a --- /dev/null +++ b/TbxEditor/Form1.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TbxEditor +{ + public partial class Form1 : Form + { + private const int THUMBNAIL_SIZE = 64; + + private string tempDir; + + private List images = new List(); + + private string _currentFileName; + private string CurrentFileName + { + get => _currentFileName; + set + { + _currentFileName = value; + this.Text = "TbxEditor - " + value; + } + } + + private static ImageList CreateImageList() => + new ImageList() + { + ColorDepth = ColorDepth.Depth32Bit, + ImageSize = new Size(THUMBNAIL_SIZE, THUMBNAIL_SIZE) + }; + + private static Bitmap GetThumbnail(Bitmap bmp, int size) + { + Rectangle target = new Rectangle(); + if (bmp.Width > bmp.Height) + { + target.X = 0; + target.Width = size; + float ratio = (float)size / (float)bmp.Width; + float h = bmp.Height * ratio; + target.Y = (size / 2) - (int)(h / 2); + target.Height = (int)h; + } + else + { + target.Y = 0; + target.Height = size; + float ratio = (float)size / (float)bmp.Height; + float w = bmp.Width * ratio; + target.X = (size / 2) - (int)(w / 2); + target.Width = (int)w; + } + Bitmap res = new Bitmap(size, size, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using (Graphics gfx = Graphics.FromImage(res)) + gfx.DrawImage(bmp, target); + return res; + } + + public Form1(string[] args) + { + InitializeComponent(); + listView.LargeImageList = CreateImageList(); + + tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + if (args?.Length > 0) + { + LoadFiles(args); + UpdateImages(); + } + } + + private void RefreshImages() + { + listView.Items.Clear(); + ImageList imgs = CreateImageList(); + for (int i = 0; i < images.Count; i++) + { + listView.Items.Add((i + 1).ToString(), i); + imgs.Images.Add(GetThumbnail(images[i], THUMBNAIL_SIZE)); + } + listView.LargeImageList = imgs; + } + + private void UpdateImages() + { + while (listView.Items.Count > images.Count) + listView.Items.RemoveAt(listView.Items.Count - 1); + for (int i = listView.Items.Count; i < images.Count; i++) + listView.Items.Add((i + 1).ToString(), i); + + listView.LargeImageList = CreateImageList(); + for (int i = 0; i < images.Count; i++) + listView.LargeImageList.Images.Add(GetThumbnail(images[i], THUMBNAIL_SIZE)); + } + + public void LoadFiles(IEnumerable filenames) + { + List failed = new List(); + foreach (var fn in filenames) + { + Bitmap bmp = null; + try + { + bmp = new Bitmap(fn); + } + catch (Exception) + { + try + { + bmp = TBG.FromFile(fn).ToBitmap(); + } + catch (Exception) + { + try + { + var bmps = TBX.ReadBitmaps(fn); + images.AddRange(bmps); + CurrentFileName = System.IO.Path.GetFileNameWithoutExtension(fn); + continue; + } + catch (Exception) + { + failed.Add(fn); + continue; + } + } + } + images.Add(bmp); + } + if (failed.Count > 0) + { + MessageBox.Show( + "Could not load the following files as images:\n\n" + string.Join("\n", failed), + "Error loading images!", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + + void RemoveSelected() + { + if (listView.SelectedItems.Count > 0) + { + List tmp = new List(); + for (int i = 0; i < images.Count; i++) + { + if (!listView.SelectedIndices.Contains(i)) + tmp.Add(images[i]); + } + images = tmp; + listView.SelectedIndices.Clear(); + UpdateImages(); + } + } + + + + private void btnOpen_Click(object sender, EventArgs e) + { + OpenFileDialog ofd = new OpenFileDialog(); + ofd.Filter = "TBX Image Archvies (*.tbx)|*.tbx|All Files (*.*)|*.*"; + if (ofd.ShowDialog() == DialogResult.OK) + { + var tbgs = TBX.Read(ofd.FileName); + images = new List(); + foreach (var tbg in tbgs) + images.Add(tbg.ToBitmap()); + CurrentFileName = System.IO.Path.GetFileNameWithoutExtension(ofd.FileName); + RefreshImages(); + } + } + + private void listView_DragOver(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + e.Effect = DragDropEffects.Copy; + } + + private void listView_DragDrop(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + var files = e.Data.GetData(DataFormats.FileDrop) as string[]; + if (files != null && files.Length > 0) + { + List filesToLoad = new List(); + foreach (string f in files) + { + // only add new files (don't allow self-drop) + if (!Path.Equals(Path.GetDirectoryName(f), tempDir)) + filesToLoad.Add(f); + } + LoadFiles(filesToLoad); + UpdateImages(); + } + } + } + + private void listView_SelectedIndexChanged(object sender, EventArgs e) + { + if (listView.SelectedItems.Count > 0) + btnRemove.Enabled = true; + else btnRemove.Enabled = false; + if (listView.SelectedIndices.Count == 1) + { + int i = listView.SelectedIndices[0]; + if (images.Count >= i) + pictureBox1.Image = images[i]; + btnUp.Enabled = true; + btnDown.Enabled = true; + } + else + { + btnUp.Enabled = false; + btnDown.Enabled = false; + } + } + + private void btnNew_Click(object sender, EventArgs e) + { + this.listView.Items.Clear(); + this.pictureBox1.Image = null; + this.images.Clear(); + } + + private void btnImport_Click(object sender, EventArgs e) + { + OpenFileDialog ofd = new OpenFileDialog(); + ofd.Filter = "Images (*.png, *.jpg, *.jpeg, *.bmp, *.tbp, *.tbx)|*.png;*.jpg;*.jpeg;*.bmp;*.tbp;*.tbx|All Files (*.*)|*.*"; + ofd.Multiselect = true; + if (ofd.ShowDialog() == DialogResult.OK) + { + LoadFiles(ofd.FileNames); + UpdateImages(); + } + } + + private void btnExport_Click(object sender, EventArgs e) + { + if (listView.Items.Count == 0) + { + MessageBox.Show("You should load some images before exporting them...", + "What?", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + try + { + string prefix = CurrentFileName ?? "image"; + if (listView.SelectedIndices.Count == 1) + { + Bitmap bmp = images[listView.SelectedIndices[0]]; + SaveFileDialog sfd = new SaveFileDialog(); + sfd.DefaultExt = ".png"; + sfd.Filter = "PNG File (*.png)|*.png"; + sfd.FileName = $"{prefix}_{listView.SelectedIndices[0]}.png"; + if (sfd.ShowDialog() == DialogResult.OK) + bmp.Save(sfd.FileName); + } + else + { + FolderBrowserDialog fbd = new FolderBrowserDialog(); + fbd.Description = "Please select a place where I should extract the images to."; + if (fbd.ShowDialog() == DialogResult.OK) + { + if (listView.SelectedIndices.Count == 0) // none selected, export all + { + for (int i = 0; i < images.Count; i++) + images[i].Save(Path.Combine(fbd.SelectedPath, $"{prefix}_{i}.png")); + } + else + { + foreach (int i in listView.SelectedIndices) + images[i].Save(Path.Combine(fbd.SelectedPath, $"{prefix}_{i}.png")); + } + } + } + } + catch (Exception exc) + { + MessageBox.Show($"Oops... Something went wrong, please let me know. Error message:\n\n{exc.Message}\n\n{exc.StackTrace}", + "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void btnRemove_Click(object sender, EventArgs e) + { + RemoveSelected(); + } + + private void Form1_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Delete) + RemoveSelected(); + } + + private void listView_ItemDrag(object sender, ItemDragEventArgs e) + { + List filenames = new List(); + string prefix = CurrentFileName ?? "image"; + foreach (int i in listView.SelectedIndices) + { + string fn = Path.Combine(tempDir, $"{prefix}_{i}.png"); + images[i].Save(fn); + filenames.Add(fn); + } + + DoDragDrop(new DataObject(DataFormats.FileDrop, filenames.ToArray()), DragDropEffects.Copy); + } + + private void Form1_FormClosing(object sender, FormClosingEventArgs e) + { + try + { + Directory.Delete(tempDir, true); + } + catch (Exception) { } + } + + private void btnUp_Click(object sender, EventArgs e) + { + if (listView.SelectedItems.Count == 1) + { + int i = listView.SelectedIndices[0]; + int j = i - 1; + if (j >= 0) + { + var tmp = images[j]; + images[j] = images[i]; + images[i] = tmp; + listView.SelectedIndices.Clear(); + listView.SelectedIndices.Add(j); + UpdateImages(); + } + } + } + + private void btnDown_Click(object sender, EventArgs e) + { + if (listView.SelectedItems.Count == 1) + { + int i = listView.SelectedIndices[0]; + int j = i + 1; + if (j < listView.Items.Count) + { + var tmp = images[j]; + images[j] = images[i]; + images[i] = tmp; + listView.SelectedIndices.Clear(); + listView.SelectedIndices.Add(j); + UpdateImages(); + } + } + } + + private void btnSave_Click(object sender, EventArgs e) + { + SaveFileDialog sfd = new SaveFileDialog(); + sfd.FileName = CurrentFileName == null ? "images.tbx" : CurrentFileName + ".tbx"; + sfd.Filter = "TBX File Archive (*.tbx)|*.tbx"; + sfd.DefaultExt = ".tbx"; + if (sfd.ShowDialog() == DialogResult.OK) + { + try + { + TBX.Write(sfd.FileName, images.ToArray()); + } + catch (Exception exc) + { + MessageBox.Show($"Oops... Something went wrong, please let me know. Error message:\n\n{exc.Message}\n\n{exc.StackTrace}", + "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } +} \ No newline at end of file diff --git a/TbxEditor/Form1.resx b/TbxEditor/Form1.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/TbxEditor/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TbxEditor/Program.cs b/TbxEditor/Program.cs new file mode 100644 index 0000000..9eb4fe0 --- /dev/null +++ b/TbxEditor/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TbxEditor +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1(args)); + } + } +} diff --git a/TbxEditor/Properties/AssemblyInfo.cs b/TbxEditor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1e76011 --- /dev/null +++ b/TbxEditor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TbxEditor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TbxEditor")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("db8e83c0-5ac6-40ad-8247-4b29f340c404")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TbxEditor/Properties/Resources.Designer.cs b/TbxEditor/Properties/Resources.Designer.cs new file mode 100644 index 0000000..1dab0be --- /dev/null +++ b/TbxEditor/Properties/Resources.Designer.cs @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +namespace TbxEditor.Properties +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TbxEditor.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/TbxEditor/Properties/Resources.resx b/TbxEditor/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/TbxEditor/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TbxEditor/Properties/Settings.Designer.cs b/TbxEditor/Properties/Settings.Designer.cs new file mode 100644 index 0000000..dca072c --- /dev/null +++ b/TbxEditor/Properties/Settings.Designer.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +namespace TbxEditor.Properties +{ + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/TbxEditor/Properties/Settings.settings b/TbxEditor/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/TbxEditor/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/TbxEditor/TBG.cs b/TbxEditor/TBG.cs new file mode 100644 index 0000000..9592d3c --- /dev/null +++ b/TbxEditor/TBG.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace TbxEditor +{ + public enum TBGPixelFormat + { + BGR, + BGRA + } + + public static class TBGPixelFormats + { + public static int GetSize(TBGPixelFormat f) + { + switch (f) + { + case TBGPixelFormat.BGR: + return 3; + case TBGPixelFormat.BGRA: + return 4; + } + throw new ArgumentException(); + } + + public static byte GetBinaryRepresentation(TBGPixelFormat f) + { + switch (f) + { + case TBGPixelFormat.BGR: + return 0x98; + case TBGPixelFormat.BGRA: + return 0x0C; + } + throw new ArgumentException(); + } + public static TBGPixelFormat FromBinaryRepresentation(byte b) + { + switch (b) + { + case 0x98: + return TBGPixelFormat.BGR; + case 0x0C: + return TBGPixelFormat.BGRA; + } + throw new ArgumentException("Unknown TBG color type!"); + } + } + + public class TBG + { + private const int HEADER_SIZE = 828; + + private byte[] _data; + + public byte[] Data + { + get { return _data; } + set + { + int pixelSize = TBGPixelFormats.GetSize(PixelFormat); + if (value.Length != pixelSize * this.Size.Width * this.Size.Height) + throw new ArgumentException("Pixel data does not match size and format!"); + _data = value; + } + } + + public TBGPixelFormat PixelFormat { get; } + + public Size Size { get; } + public float ResX { get; set; } = 1.0f; + public float ResY { get; set; } = 1.0f; + + public int ImageCount { get; } = 1; + + public TBG(Size size, TBGPixelFormat format, byte[] data) + { + this.Size = size; + this.PixelFormat = format; + this.Data = data; + } + public TBG(Size size, TBGPixelFormat format) + { + this.Size = size; + this.PixelFormat = format; + int pixelSize = TBGPixelFormats.GetSize(format); + this._data = new byte[size.Width * size.Height * pixelSize]; + } + + public static TBG FromFile(string filename) + { + using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read)) + using (BinaryReader file = new BinaryReader(fs)) + { + byte[] magic = file.ReadBytes(4); + if (Encoding.ASCII.GetString(magic) != "tbg\0") + throw new InvalidDataException("Invalid magic number!"); + uint dataOffset = file.ReadUInt32(); + int dataLength = file.ReadInt32(); + int width = file.ReadInt32(); + int height = file.ReadInt32(); + byte[] flags = file.ReadBytes(4); + int imageCount = file.ReadInt32(); + long unknown = file.ReadInt64(); + float resX = file.ReadSingle(); + float resY = file.ReadSingle(); + + var format = TBGPixelFormats.FromBinaryRepresentation(flags[3]); + + file.BaseStream.Position = dataOffset; + byte[] data = file.ReadBytes(dataLength); + var tbg = new TBG(new Size(width, height), format, data) + { + ResX = resX, + ResY = resY + }; + return tbg; + } + } + + public static TBG FromBuffer(byte[] fileData) + { + if (Encoding.ASCII.GetString(fileData, 0, 4) != "tbg\0") + throw new InvalidDataException("Invalid magic number!"); + uint dataOffset = BitConverter.ToUInt32(fileData, 0x04); + int dataLength = BitConverter.ToInt32(fileData, 0x08); + int width = BitConverter.ToInt32(fileData, 0x0C); + int height = BitConverter.ToInt32(fileData, 0x10); + byte colorByte = fileData[0x14 + 3]; + float resX = BitConverter.ToSingle(fileData, 0x24); + float resY = BitConverter.ToSingle(fileData, 0x28); + var format = TBGPixelFormats.FromBinaryRepresentation(colorByte); + var data = new byte[dataLength]; + Array.Copy(fileData, dataOffset, data, 0, dataLength); + + var tbg = new TBG(new Size(width, height), format, data) + { + ResX = resX, + ResY = resY + }; + return tbg; + } + + public static TBG FromBitmap(Bitmap bmp) + { + var tbgFormat = TBGPixelFormat.BGRA; + var bmpFormat = System.Drawing.Imaging.PixelFormat.Format32bppArgb; + if (bmp.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb) + { + tbgFormat = TBGPixelFormat.BGR; + bmpFormat = System.Drawing.Imaging.PixelFormat.Format32bppRgb; + } + int pixelSize = TBGPixelFormats.GetSize(tbgFormat); + + var data = new byte[bmp.Width * bmp.Height * pixelSize]; + var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmpFormat); + Marshal.Copy(bmpData.Scan0, data, 0, data.Length); + bmp.UnlockBits(bmpData); + + return new TBG(bmp.Size, tbgFormat, data); + } + + public Bitmap ToBitmap() + { + switch (this.PixelFormat) + { + case TBGPixelFormat.BGR: + return GetBitmapBGR(Data, Size); + case TBGPixelFormat.BGRA: + return GetBitmapBGRA(Data, Size); + } + return null; + } + + private static Bitmap GetBitmapBGR(byte[] data, Size size) + { + Bitmap bmp = new Bitmap(size.Width, size.Height); + var bmpData = bmp.LockBits( + new Rectangle(0, 0, size.Width, size.Height), + System.Drawing.Imaging.ImageLockMode.WriteOnly, + System.Drawing.Imaging.PixelFormat.Format32bppRgb); + Marshal.Copy(data, 0, bmpData.Scan0, data.Length); + bmp.UnlockBits(bmpData); + return bmp; + } + + private static Bitmap GetBitmapBGRA(byte[] data, Size size) + { + Bitmap bmp = new Bitmap(size.Width, size.Height); + var bmpData = bmp.LockBits( + new Rectangle(0, 0, size.Width, size.Height), + System.Drawing.Imaging.ImageLockMode.WriteOnly, + System.Drawing.Imaging.PixelFormat.Format32bppArgb); + Marshal.Copy(data, 0, bmpData.Scan0, data.Length); + bmp.UnlockBits(bmpData); + return bmp; + } + + /// + /// Writes the TBG file to a stream using the provided BinaryWriter. + /// See https://github.com/Anonym271/7scarlet-tools/wiki/TBG-Images-%28%2A.tbg%29 for details. + /// + public void Save(BinaryWriter file) + { + var startPos = file.BaseStream.Position; + file.Write(Encoding.ASCII.GetBytes("tbg\0")); + file.Write(HEADER_SIZE); + file.Write(_data.Length); + file.Write(Size.Width); + file.Write(Size.Height); + file.Write(new byte[] { 0x00, 0x10, 0x00 }); + file.Write(TBGPixelFormats.GetBinaryRepresentation(PixelFormat)); + file.Write(1); + file.Write((long)0); + file.Write(ResX); + file.Write(ResY); + + file.BaseStream.Position = startPos + HEADER_SIZE; + file.Write(_data); + } + public void Save(string filename) + { + using (FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) + using (BinaryWriter file = new BinaryWriter(fs)) + Save(file); + } + public byte[] SaveToBuffer() + { + using (MemoryStream ms = new MemoryStream()) + using (BinaryWriter file = new BinaryWriter(ms)) + { + Save(file); + return ms.ToArray(); + } + } + } +} diff --git a/TbxEditor/TBX.cs b/TbxEditor/TBX.cs new file mode 100644 index 0000000..2a3fcc5 --- /dev/null +++ b/TbxEditor/TBX.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.Drawing; + +namespace TbxEditor +{ + public static class TBX + { + public static TBG[] Read(string filename) + { + using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read)) + using (BinaryReader file = new BinaryReader(fs)) + { + byte[] magic = file.ReadBytes(4); + if (Encoding.ASCII.GetString(magic) != "tbx\0") + new InvalidDataException("Invalid magic number!"); + uint dataOffset = file.ReadUInt32(); + int fileCount = file.ReadInt32(); + uint tableOffset = file.ReadUInt32(); + + fs.Position = tableOffset; + + var table = new (uint, uint)[fileCount]; + for (int i = 0; i < fileCount; i++) + { + table[i].Item1 = file.ReadUInt32(); + table[i].Item2 = file.ReadUInt32(); + } + + var tbgs = new TBG[fileCount]; + for(int i = 0; i < fileCount; i++) + { + fs.Position = table[i].Item1; + byte[] data = file.ReadBytes((int)table[i].Item2); + tbgs[i] = TBG.FromBuffer(data); + } + + return tbgs; + } + } + + public static Bitmap[] ReadBitmaps(string filename) + { + var tbgs = Read(filename); + var bmps = new Bitmap[tbgs.Length]; + for (int i = 0; i < tbgs.Length; i++) + bmps[i] = tbgs[i].ToBitmap(); + return bmps; + } + + public static void Write(string filename, TBG[] images) + { + using (FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) + using (BinaryWriter file = new BinaryWriter(fs)) + { + file.Write(Encoding.ASCII.GetBytes("tbx\0")); + int dataOffset = 16 + 8 * images.Length; // data offset = headerSize + entrySize * entryCount + file.Write(dataOffset); + file.Write(images.Length); + file.Write(16); // table offset + + var table = new (uint, uint)[images.Length]; + fs.Position = dataOffset; + for (int i = 0; i < images.Length; i++) + { + var start = fs.Position; + table[i].Item1 = (uint)start; + images[i].Save(file); + table[i].Item2 = (uint)(fs.Position - start); + } + + fs.Position = 16; + foreach (var (pos, offs) in table) + { + file.Write(pos); + file.Write(offs); + } + } + } + public static void Write(string filename, Bitmap[] images) + { + TBG[] tbgs = new TBG[images.Length]; + for (int i = 0; i < images.Length; i++) + tbgs[i] = TBG.FromBitmap(images[i]); + Write(filename, tbgs); + } + } +} diff --git a/TbxEditor/TbxEditor.csproj b/TbxEditor/TbxEditor.csproj new file mode 100644 index 0000000..4d1090a --- /dev/null +++ b/TbxEditor/TbxEditor.csproj @@ -0,0 +1,85 @@ + + + + + Debug + AnyCPU + {DB8E83C0-5AC6-40AD-8247-4B29F340C404} + WinExe + TbxEditor + TbxEditor + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + \ No newline at end of file