From 10838f33a9c7f7f68a7b54704d42e5b29f8284a2 Mon Sep 17 00:00:00 2001 From: NinjaPug <36635276+programmingPug@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:26:10 -0400 Subject: [PATCH] Initial upload of project --- .gitignore | 3 + FontGlyphExporter.csproj | 8 ++ FontGlyphExporter.sln | 25 ++++ MainForm.Designer.cs | 191 +++++++++++++++++++++++++ MainForm.cs | 291 +++++++++++++++++++++++++++++++++++++++ Program.cs | 16 +++ readme.md | 120 ++++++++++++++++ 7 files changed, 654 insertions(+) create mode 100644 .gitignore create mode 100644 FontGlyphExporter.csproj create mode 100644 FontGlyphExporter.sln create mode 100644 MainForm.Designer.cs create mode 100644 MainForm.cs create mode 100644 Program.cs create mode 100644 readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a848e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bin +/obj +/.vs diff --git a/FontGlyphExporter.csproj b/FontGlyphExporter.csproj new file mode 100644 index 0000000..4eb9ee6 --- /dev/null +++ b/FontGlyphExporter.csproj @@ -0,0 +1,8 @@ + + + + WinExe + net6.0-windows + true + + diff --git a/FontGlyphExporter.sln b/FontGlyphExporter.sln new file mode 100644 index 0000000..ffc3f54 --- /dev/null +++ b/FontGlyphExporter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35828.75 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontGlyphExporter", "FontGlyphExporter.csproj", "{2E8081B2-DFAC-3227-72D0-366CF7F45501}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2E8081B2-DFAC-3227-72D0-366CF7F45501}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E8081B2-DFAC-3227-72D0-366CF7F45501}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E8081B2-DFAC-3227-72D0-366CF7F45501}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E8081B2-DFAC-3227-72D0-366CF7F45501}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {17BAE8BB-58F1-4268-9740-4AA4ACE38243} + EndGlobalSection +EndGlobal diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs new file mode 100644 index 0000000..055fef6 --- /dev/null +++ b/MainForm.Designer.cs @@ -0,0 +1,191 @@ +namespace FontGlyphExporter +{ + partial class MainForm + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.TextBox txtTTF; + private System.Windows.Forms.TextBox txtCSS; + private System.Windows.Forms.Button btnBrowseTTF; + private System.Windows.Forms.Button btnBrowseCSS; + private System.Windows.Forms.Button btnLoad; + private System.Windows.Forms.Button btnExport; + private System.Windows.Forms.FlowLayoutPanel iconFlowPanel; + private System.Windows.Forms.Label lblCounter; + private System.Windows.Forms.ComboBox cboRegexPattern; + private System.Windows.Forms.Label lblRegexPattern; + private System.Windows.Forms.TextBox txtCustomRegex; + private System.Windows.Forms.Label lblCustomRegex; + private System.Windows.Forms.Panel pnlSizes; + private System.Windows.Forms.Label lblSizes; + private System.Windows.Forms.CheckBox chkSize4; + private System.Windows.Forms.CheckBox chkSize8; + private System.Windows.Forms.CheckBox chkSize12; + private System.Windows.Forms.CheckBox chkSize24; + private System.Windows.Forms.Label lblSelectedSizes; + + private void InitializeComponent() + { + this.txtTTF = new System.Windows.Forms.TextBox(); + this.txtCSS = new System.Windows.Forms.TextBox(); + this.btnBrowseTTF = new System.Windows.Forms.Button(); + this.btnBrowseCSS = new System.Windows.Forms.Button(); + this.btnLoad = new System.Windows.Forms.Button(); + this.btnExport = new System.Windows.Forms.Button(); + this.lblCounter = new System.Windows.Forms.Label(); + this.iconFlowPanel = new System.Windows.Forms.FlowLayoutPanel(); + this.cboRegexPattern = new System.Windows.Forms.ComboBox(); + this.lblRegexPattern = new System.Windows.Forms.Label(); + this.txtCustomRegex = new System.Windows.Forms.TextBox(); + this.lblCustomRegex = new System.Windows.Forms.Label(); + this.pnlSizes = new System.Windows.Forms.Panel(); + this.lblSizes = new System.Windows.Forms.Label(); + this.chkSize4 = new System.Windows.Forms.CheckBox(); + this.chkSize8 = new System.Windows.Forms.CheckBox(); + this.chkSize12 = new System.Windows.Forms.CheckBox(); + this.chkSize24 = new System.Windows.Forms.CheckBox(); + this.lblSelectedSizes = new System.Windows.Forms.Label(); + this.pnlSizes.SuspendLayout(); + this.SuspendLayout(); + // + // txtTTF + this.txtTTF.Location = new System.Drawing.Point(12, 12); + this.txtTTF.Size = new System.Drawing.Size(400, 23); + // + // btnBrowseTTF + this.btnBrowseTTF.Location = new System.Drawing.Point(418, 12); + this.btnBrowseTTF.Size = new System.Drawing.Size(100, 23); + this.btnBrowseTTF.Text = "Browse TTF"; + this.btnBrowseTTF.Click += new System.EventHandler(this.btnBrowseTTF_Click); + // + // txtCSS + this.txtCSS.Location = new System.Drawing.Point(12, 41); + this.txtCSS.Size = new System.Drawing.Size(400, 23); + // + // btnBrowseCSS + this.btnBrowseCSS.Location = new System.Drawing.Point(418, 41); + this.btnBrowseCSS.Size = new System.Drawing.Size(100, 23); + this.btnBrowseCSS.Text = "Browse CSS"; + this.btnBrowseCSS.Click += new System.EventHandler(this.btnBrowseCSS_Click); + // + // lblRegexPattern + this.lblRegexPattern.Location = new System.Drawing.Point(540, 12); + this.lblRegexPattern.Size = new System.Drawing.Size(80, 23); + this.lblRegexPattern.Text = "Regex Pattern:"; + this.lblRegexPattern.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // cboRegexPattern + this.cboRegexPattern.Location = new System.Drawing.Point(625, 12); + this.cboRegexPattern.Size = new System.Drawing.Size(150, 23); + this.cboRegexPattern.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cboRegexPattern.Items.AddRange(new object[] { + "Font Awesome 5/6", + "Font Awesome 4", + "Material Icons", + "Custom" + }); + this.cboRegexPattern.SelectedIndexChanged += new System.EventHandler(this.cboRegexPattern_SelectedIndexChanged); + // + // lblCustomRegex + this.lblCustomRegex.Location = new System.Drawing.Point(540, 41); + this.lblCustomRegex.Size = new System.Drawing.Size(80, 23); + this.lblCustomRegex.Text = "Custom Regex:"; + this.lblCustomRegex.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.lblCustomRegex.Visible = false; + // + // txtCustomRegex + this.txtCustomRegex.Location = new System.Drawing.Point(625, 41); + this.txtCustomRegex.Size = new System.Drawing.Size(150, 23); + this.txtCustomRegex.Visible = false; + // + // lblSizes + this.lblSizes.Location = new System.Drawing.Point(418, 70); + this.lblSizes.Size = new System.Drawing.Size(100, 23); + this.lblSizes.Text = "Export Sizes:"; + this.lblSizes.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlSizes + this.pnlSizes.Location = new System.Drawing.Point(520, 70); + this.pnlSizes.Size = new System.Drawing.Size(255, 25); + // + // chkSize4 + this.chkSize4.Location = new System.Drawing.Point(5, 3); + this.chkSize4.Size = new System.Drawing.Size(50, 20); + this.chkSize4.Text = "4x4"; + this.chkSize4.CheckedChanged += new System.EventHandler(this.sizeCheckbox_CheckedChanged); + this.pnlSizes.Controls.Add(this.chkSize4); + // + // chkSize8 + this.chkSize8.Location = new System.Drawing.Point(65, 3); + this.chkSize8.Size = new System.Drawing.Size(50, 20); + this.chkSize8.Text = "8x8"; + this.chkSize8.CheckedChanged += new System.EventHandler(this.sizeCheckbox_CheckedChanged); + this.pnlSizes.Controls.Add(this.chkSize8); + // + // chkSize12 + this.chkSize12.Location = new System.Drawing.Point(125, 3); + this.chkSize12.Size = new System.Drawing.Size(60, 20); + this.chkSize12.Text = "12x12"; + this.chkSize12.CheckedChanged += new System.EventHandler(this.sizeCheckbox_CheckedChanged); + this.pnlSizes.Controls.Add(this.chkSize12); + // + // chkSize24 + this.chkSize24.Location = new System.Drawing.Point(195, 3); + this.chkSize24.Size = new System.Drawing.Size(60, 20); + this.chkSize24.Text = "24x24"; + this.chkSize24.CheckedChanged += new System.EventHandler(this.sizeCheckbox_CheckedChanged); + this.pnlSizes.Controls.Add(this.chkSize24); + // + // lblSelectedSizes + this.lblSelectedSizes.Location = new System.Drawing.Point(520, 95); + this.lblSelectedSizes.Size = new System.Drawing.Size(255, 15); + this.lblSelectedSizes.Text = "Selected sizes: 12, 24px"; + // + // btnLoad + this.btnLoad.Location = new System.Drawing.Point(12, 70); + this.btnLoad.Size = new System.Drawing.Size(100, 23); + this.btnLoad.Text = "Load Icons"; + this.btnLoad.Click += new System.EventHandler(this.btnLoad_Click); + // + // btnExport + this.btnExport.Location = new System.Drawing.Point(118, 70); + this.btnExport.Size = new System.Drawing.Size(120, 23); + this.btnExport.Text = "Export Selected"; + this.btnExport.Click += new System.EventHandler(this.btnExport_Click); + // + // lblCounter + this.lblCounter.Location = new System.Drawing.Point(250, 70); + this.lblCounter.Size = new System.Drawing.Size(150, 23); + this.lblCounter.Text = "Selected: 0 / 0"; + // + // iconFlowPanel + this.iconFlowPanel.Location = new System.Drawing.Point(12, 120); + this.iconFlowPanel.Size = new System.Drawing.Size(760, 430); + this.iconFlowPanel.AutoScroll = true; + this.iconFlowPanel.WrapContents = true; + this.iconFlowPanel.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight; + // + // MainForm + this.ClientSize = new System.Drawing.Size(784, 561); + this.Controls.Add(this.txtTTF); + this.Controls.Add(this.btnBrowseTTF); + this.Controls.Add(this.txtCSS); + this.Controls.Add(this.btnBrowseCSS); + this.Controls.Add(this.lblRegexPattern); + this.Controls.Add(this.cboRegexPattern); + this.Controls.Add(this.lblCustomRegex); + this.Controls.Add(this.txtCustomRegex); + this.Controls.Add(this.lblSizes); + this.Controls.Add(this.pnlSizes); + this.Controls.Add(this.lblSelectedSizes); + this.Controls.Add(this.btnLoad); + this.Controls.Add(this.btnExport); + this.Controls.Add(this.lblCounter); + this.Controls.Add(this.iconFlowPanel); + this.Name = "MainForm"; + this.Text = "Font Awesome Icon Exporter"; + this.pnlSizes.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + } + } +} \ No newline at end of file diff --git a/MainForm.cs b/MainForm.cs new file mode 100644 index 0000000..8f032df --- /dev/null +++ b/MainForm.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Text; +using System.IO; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +namespace FontGlyphExporter +{ + public partial class MainForm : Form + { + private string fontPath = ""; + private string cssPath = ""; + private List allIcons = new(); + private List selectedIcons = new(); + private PrivateFontCollection fontCollection = new(); + private Font font; + private Dictionary regexPatterns = new(); + private List selectedSizes = new(); + + public MainForm() + { + InitializeComponent(); + InitializeRegexPatterns(); + InitializeSizeCheckboxes(); + cboRegexPattern.SelectedIndex = 0; + } + + private void InitializeRegexPatterns() + { + regexPatterns.Add("Font Awesome 5/6", @"\.fa-([a-z0-9-]+)\s*{[^}]*--fa:\s*""\\(f[a-f0-9]{3,4})"""); + regexPatterns.Add("Font Awesome 4", @"\.fa-([a-z0-9-]+):before\s*{[^}]*content:\s*""\\(f[a-f0-9]{3,4})"""); + regexPatterns.Add("Material Icons", @"\.material-icons-([a-z0-9-]+):before\s*{[^}]*content:\s*""\\(e[a-f0-9]{3,4})"""); + regexPatterns.Add("Custom", ""); + } + + private void InitializeSizeCheckboxes() + { + chkSize4.Tag = 4; + chkSize8.Tag = 8; + chkSize12.Tag = 12; + chkSize24.Tag = 24; + + // Default selection + chkSize12.Checked = true; + chkSize24.Checked = true; + UpdateSelectedSizes(); + } + + private void UpdateSelectedSizes() + { + selectedSizes.Clear(); + + foreach (Control c in pnlSizes.Controls) + { + if (c is CheckBox chk && chk.Checked && chk.Tag is int size) + { + selectedSizes.Add(size); + } + } + + // Update the selected sizes label + lblSelectedSizes.Text = selectedSizes.Count > 0 + ? $"Selected sizes: {string.Join(", ", selectedSizes)}px" + : "No sizes selected"; + } + + private void btnBrowseTTF_Click(object sender, EventArgs e) + { + using OpenFileDialog ofd = new() { Filter = "Font Files (*.ttf)|*.ttf" }; + if (ofd.ShowDialog() == DialogResult.OK) + { + fontPath = ofd.FileName; + txtTTF.Text = fontPath; + } + } + + private void btnBrowseCSS_Click(object sender, EventArgs e) + { + using OpenFileDialog ofd = new() { Filter = "CSS Files (*.css)|*.css" }; + if (ofd.ShowDialog() == DialogResult.OK) + { + cssPath = ofd.FileName; + txtCSS.Text = cssPath; + } + } + + private void btnLoad_Click(object sender, EventArgs e) + { + if (!File.Exists(fontPath) || !File.Exists(cssPath)) + { + MessageBox.Show("Please select valid CSS and TTF files."); + return; + } + + fontCollection = new PrivateFontCollection(); + fontCollection.AddFontFile(fontPath); + font = new Font(fontCollection.Families[0], 16); + + string css = File.ReadAllText(cssPath); + string patternToUse; + + if (cboRegexPattern.SelectedItem.ToString() == "Custom") + { + patternToUse = txtCustomRegex.Text; + if (string.IsNullOrWhiteSpace(patternToUse)) + { + MessageBox.Show("Please enter a valid regex pattern."); + return; + } + } + else + { + patternToUse = regexPatterns[cboRegexPattern.SelectedItem.ToString()]; + } + + try + { + var matches = Regex.Matches(css, patternToUse, RegexOptions.IgnoreCase); + + allIcons.Clear(); + foreach (Match match in matches) + { + if (match.Groups.Count < 3) continue; + string name = match.Groups[1].Value; + string unicode = match.Groups[2].Value; + allIcons.Add(new IconInfo { name = name, unicode = unicode }); + } + + if (allIcons.Count == 0) + { + MessageBox.Show("No icons found with the selected pattern. Try a different pattern or check your CSS file."); + return; + } + + PopulateGrid(); + } + catch (ArgumentException ex) + { + MessageBox.Show($"Invalid regex pattern: {ex.Message}"); + } + } + + private void PopulateGrid() + { + iconFlowPanel.Controls.Clear(); + selectedIcons.Clear(); + UpdateCounter(); + + foreach (var icon in allIcons) + { + var panel = new Panel + { + Width = 80, + Height = 100, + Margin = new Padding(5), + BorderStyle = BorderStyle.FixedSingle, + Tag = icon + }; + + var bmp = new Bitmap(48, 48); + using (Graphics g = Graphics.FromImage(bmp)) + { + g.Clear(Color.White); + g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; + string glyph = char.ConvertFromUtf32(Convert.ToInt32(icon.unicode, 16)); + g.DrawString(glyph, font, Brushes.Black, 0, 0); + } + + var pic = new PictureBox + { + Image = bmp, + SizeMode = PictureBoxSizeMode.CenterImage, + Width = 48, + Height = 48, + Top = 5, + Left = 15 + }; + + var lbl = new Label + { + Text = icon.name, + Width = 70, + Top = 60, + Left = 5, + TextAlign = ContentAlignment.MiddleCenter + }; + + panel.Controls.Add(pic); + panel.Controls.Add(lbl); + + panel.Click += (s, e) => + { + if (selectedIcons.Contains(icon)) + { + selectedIcons.Remove(icon); + panel.BackColor = SystemColors.Control; + } + else + { + selectedIcons.Add(icon); + panel.BackColor = Color.LightBlue; + } + UpdateCounter(); + }; + + iconFlowPanel.Controls.Add(panel); + } + } + + private void UpdateCounter() + { + lblCounter.Text = $"Selected: {selectedIcons.Count} / {allIcons.Count}"; + } + + private void btnExport_Click(object sender, EventArgs e) + { + if (selectedIcons.Count == 0) + { + MessageBox.Show("Please select at least one icon to export."); + return; + } + + if (selectedSizes.Count == 0) + { + MessageBox.Show("Please select at least one size for export."); + return; + } + + var icons = new List(); + var outputDir = Path.Combine("output", "icons"); + Directory.CreateDirectory(outputDir); + + foreach (var icon in selectedIcons) + { + foreach (var size in selectedSizes) + { + string glyph = char.ConvertFromUtf32(Convert.ToInt32(icon.unicode, 16)); + string file = $"icon_{icon.name}_{size}x{size}.bin"; + string path = Path.Combine(outputDir, file); + + using Bitmap bmp = new(size, size); + using Graphics g = Graphics.FromImage(bmp); + g.Clear(Color.White); + g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; + g.DrawString(glyph, new Font(font.FontFamily, size), Brushes.Black, 0, 0); + + using FileStream fs = new(path, FileMode.Create); + for (int y = 0; y < bmp.Height; y++) + { + for (int x = 0; x < bmp.Width; x += 8) + { + byte b = 0; + for (int bit = 0; bit < 8 && (x + bit) < bmp.Width; bit++) + { + var pixel = bmp.GetPixel(x + bit, y); + if (pixel.R < 128) b |= (byte)(1 << bit); + } + fs.WriteByte(b); + } + } + + icons.Add(new { name = icon.name, file = file, width = size, height = size }); + } + } + + File.WriteAllText("output/icons_index.json", JsonSerializer.Serialize(icons, new JsonSerializerOptions { WriteIndented = true })); + MessageBox.Show("Export complete!"); + } + + private void cboRegexPattern_SelectedIndexChanged(object sender, EventArgs e) + { + bool isCustom = cboRegexPattern.SelectedItem.ToString() == "Custom"; + txtCustomRegex.Visible = isCustom; + lblCustomRegex.Visible = isCustom; + } + + private void sizeCheckbox_CheckedChanged(object sender, EventArgs e) + { + UpdateSelectedSizes(); + } + + public class IconInfo + { + public string name { get; set; } + public string unicode { get; set; } + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..542a982 --- /dev/null +++ b/Program.cs @@ -0,0 +1,16 @@ + +using System; +using System.Windows.Forms; + +namespace FontGlyphExporter +{ + internal static class Program + { + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..b37b675 --- /dev/null +++ b/readme.md @@ -0,0 +1,120 @@ +# Font Glyph Exporter + +A Windows Forms application for browsing, selecting, and exporting Font Awesome icons (or other icon fonts) as binary files for use in embedded systems, microcontrollers, or other display applications. + +![Application Screenshot](screenshot.png) + +## Features + +- Browse and preview icons from Font TTF files +- Support for multiple font formats through configurable regex patterns +- Easy icon selection with visual preview +- Export icons in various sizes (4x4, 8x8, 12x12, 24x24) +- Exports as 1-bit monochrome binary format for efficiency +- Output indexed JSON for easy integration with your applications + +## Requirements + +- .NET 6.0 or later +- Windows OS +- Font TTF file +- Corresponding CSS file with icon mappings + +## Getting Started + +1. Download the latest release or build from source +2. Run the application +3. Browse to select your TTF font file +4. Browse to select your CSS file with icon mappings +5. Select the appropriate regex pattern for your font or create a custom one +6. Click "Load Icons" to view all available icons +7. Select the icons you want to export +8. Choose desired output sizes using the checkboxes +9. Click "Export Selected" to generate the binary files + +## Building from Source + +```bash +# Clone the repository +git clone https://github.com/programmingPug/FontGlyphExporter.git + +# Navigate to the project directory +cd FontGlyphExporter + +# Build the project +dotnet build + +# Run the application +dotnet run +``` + +## How It Works + +The application: + +1. Loads the TTF font file into a private font collection +2. Parses the CSS file using regex to extract icon names and Unicode values +3. Renders each icon in the UI for preview +4. When exporting, creates a 1-bit monochrome bitmap for each selected icon at each selected size +5. Saves these bitmaps as binary files, where each bit represents a pixel (1 = black, 0 = white) +6. Generates a JSON index file mapping icon names to their file information + +## Regex Patterns + +The application includes several predefined regex patterns: + +- **Font Awesome 5/6**: `.fa-([a-z0-9-]+)\s*{[^}]*--fa:\s*"\\(f[a-f0-9]{3,4})"` +- **Font Awesome 4**: `.fa-([a-z0-9-]+):before\s*{[^}]*content:\s*"\\(f[a-f0-9]{3,4})"` +- **Material Icons**: `.material-icons-([a-z0-9-]+):before\s*{[^}]*content:\s*"\\(e[a-f0-9]{3,4})"` +- **Custom**: Enter your own regex pattern + +The regex should capture two groups: +1. The icon name +2. The Unicode hex value + +## Output Format + +### Binary Files + +The binary files are stored in the `output/icons` directory. Each bit represents a pixel: +- 1 = Black pixel (icon) +- 0 = White pixel (background) + +Each row is padded to a byte boundary, with bits ordered from least significant to most significant. + +### JSON Index + +The program generates a JSON index file (`output/icons_index.json`) with metadata for all exported icons: + +```json +[ + { + "name": "calendar", + "file": "icon_calendar_12x12.bin", + "width": 12, + "height": 12 + }, + { + "name": "calendar", + "file": "icon_calendar_24x24.bin", + "width": 24, + "height": 24 + } +] +``` + +## Use Cases + +- Embedded systems with monochrome displays +- Microcontroller applications +- LCD/OLED screens +- E-paper displays +- Any application requiring lightweight icon representation + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- .NET Windows Forms for the UI framework