From 6e8dbc58104182a21e787c4b0486faba399293ac Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 17 Jun 2026 14:33:21 +0200 Subject: [PATCH 1/4] change json models to specified --- Models/AppJsonContext.cs | 14 ++++++++++++++ Models/FieldConfig.cs | 2 +- Models/UiResponse.cs | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Models/AppJsonContext.cs create mode 100644 Models/UiResponse.cs diff --git a/Models/AppJsonContext.cs b/Models/AppJsonContext.cs new file mode 100644 index 0000000..c240e2e --- /dev/null +++ b/Models/AppJsonContext.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; +using AvifGenerator.Models; + +namespace AvifGenerator.Utils +{ + [JsonSourceGenerationOptions( + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(UiResponse))] + internal partial class AppJsonContext : JsonSerializerContext + { + } +} diff --git a/Models/FieldConfig.cs b/Models/FieldConfig.cs index 68814b1..fba4da1 100644 --- a/Models/FieldConfig.cs +++ b/Models/FieldConfig.cs @@ -21,4 +21,4 @@ public class FieldConfig [JsonPropertyName("BoxWidth")] public int? BoxWidth { get; set; } [JsonPropertyName("BoxHeight")] public int? BoxHeight { get; set; } } -} \ No newline at end of file +} diff --git a/Models/UiResponse.cs b/Models/UiResponse.cs new file mode 100644 index 0000000..6868b7f --- /dev/null +++ b/Models/UiResponse.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace AvifGenerator.Models +{ + public class UiResponse + { + [JsonPropertyName("action")] public string Action { get; set; } = string.Empty; + [JsonPropertyName("success")] public bool? Success { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("path")] public string? Path { get; set; } + [JsonPropertyName("error")] public string? Error { get; set; } + [JsonPropertyName("templatePath")] public string? TemplatePath { get; set; } + [JsonPropertyName("config")] public Dictionary? Config { get; set; } + [JsonPropertyName("ok")] public int? Ok { get; set; } + [JsonPropertyName("fail")] public int? Fail { get; set; } + } +} From 4c86cda5c500259268ccda536b26351be1eb2825 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 17 Jun 2026 14:34:15 +0200 Subject: [PATCH 2/4] change the JSON serialization --- Program.cs | 2 +- Services/Bridge.cs | 160 +++++++++++++---------------------- Services/Executor.cs | 16 ++-- Utils/ConfigurationLoader.cs | 40 +++++++-- 4 files changed, 101 insertions(+), 117 deletions(-) diff --git a/Program.cs b/Program.cs index c5196d1..4e7bfb0 100644 --- a/Program.cs +++ b/Program.cs @@ -39,7 +39,7 @@ static int Main(string[] args) }); var serviceProvider = services.BuildServiceProvider(); - + var window = serviceProvider.GetRequiredService(); window.WaitForClose(); diff --git a/Services/Bridge.cs b/Services/Bridge.cs index f377c10..9ba1657 100644 --- a/Services/Bridge.cs +++ b/Services/Bridge.cs @@ -10,7 +10,7 @@ public class Bridge { private readonly GenerationOptions _options; private readonly Executor _executor; - public PhotinoWindow? Window { get; set;} + public PhotinoWindow? Window { get; set; } public Bridge(GenerationOptions options, Executor executor) { @@ -30,30 +30,14 @@ public void HandleMessage(object? sender, string message) switch (action) { - case "RUN": - HandleRunAction(root); - break; - case "CHOOSE_TEMPLATE": - HandleChooseTemplate(root); - break; - case "CHOOSE_CSV": - HandleChooseCsv(root); - break; - case "CHOOSE_OUTPUT": - HandleChooseOutput(root); - break; - case "LOAD_CONFIG": - HandleLoadConfig(root); - break; - case "CHOOSE_CONFIG": - HandleChooseConfig(root); - break; - case "SAVE_CONFIG": - HandleSaveConfig(root); - break; - default: - Console.WriteLine("Unknown action!"); - break; + case "RUN": HandleRunAction(root); break; + case "CHOOSE_TEMPLATE": HandleChooseTemplate(root); break; + case "CHOOSE_CSV": HandleChooseCsv(root); break; + case "CHOOSE_OUTPUT": HandleChooseOutput(root); break; + case "LOAD_CONFIG": HandleLoadConfig(root); break; + case "CHOOSE_CONFIG": HandleChooseConfig(root); break; + case "SAVE_CONFIG": HandleSaveConfig(root); break; + default: Console.WriteLine("Unknown action!"); break; } } catch (Exception ex) @@ -61,44 +45,44 @@ public void HandleMessage(object? sender, string message) Console.WriteLine($"❌ Failed to process UI message: {ex.Message}"); } } + private void HandleRunAction(JsonElement root) { HandleSaveConfig(root); - var freshConfig = ConfigurationLoader.LoadConfig(_options.ConfigPath); - if(freshConfig == null) return; + var freshConfig = ConfigurationLoader.LoadConfig(_options.ConfigPath ?? string.Empty); + if (freshConfig == null) return; Console.WriteLine("▶ Settings updated! Starting background generation task..."); - - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "GENERATING", - })); - - _executor.OnMessageReady = (message) => + + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "GENERATING" }, AppJsonContext.Default.UiResponse)); + + _executor.OnMessageReady = (msg) => { Window?.Invoke(() => { - Window?.SendWebMessage(message); + Window?.SendWebMessage(msg); }); }; Task.Run(() => _executor.ExecuteGenerationAsync(freshConfig)); } + private void HandleSaveConfig(JsonElement root) { try { - var diskConfig = ConfigurationLoader.LoadConfig(_options.ConfigPath) ?? new(); - + var diskConfig = ConfigurationLoader.LoadConfig(_options.ConfigPath ?? string.Empty) ?? new(); + var fields = root.GetProperty("data").GetRawText(); - var uiConfig = JsonSerializer.Deserialize>(fields); - if(uiConfig != null) + var uiConfig = JsonSerializer.Deserialize(fields, AppJsonContext.Default.DictionaryStringFieldConfig); + if (uiConfig != null) { foreach (var (key, uiField) in uiConfig) { - if(key == "GLOBAL_SETTINGS") continue; + if (key == "GLOBAL_SETTINGS") continue; if (!diskConfig.ContainsKey(key)) continue; - + diskConfig[key].X = uiField.X ?? diskConfig[key].X; diskConfig[key].Y = uiField.Y ?? diskConfig[key].Y; diskConfig[key].BoxWidth = uiField.BoxWidth ?? diskConfig[key].BoxWidth; @@ -107,15 +91,12 @@ private void HandleSaveConfig(JsonElement root) diskConfig[key].Color = uiField.Color ?? diskConfig[key].Color; } } - - ConfigurationLoader.SaveConfig(_options.ConfigPath, diskConfig); - - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CONFIG_SAVED", - success = true - })); - + + ConfigurationLoader.SaveConfig(_options.ConfigPath ?? string.Empty, diskConfig); + + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CONFIG_SAVED", Success = true }, AppJsonContext.Default.UiResponse)); + Console.WriteLine("✅ Config saved from UI panel"); } catch (Exception ex) @@ -123,98 +104,77 @@ private void HandleSaveConfig(JsonElement root) Console.WriteLine($"❌ SaveConfig error: {ex.Message}"); } } + private void HandleChooseCsv(JsonElement root) { var result = Dialog.FileOpen("csv"); - if(!result.IsOk) return; + if (!result.IsOk) return; _options.CsvPath = result.Path; - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CSV_CHOSEN", - name = Path.GetFileName(result.Path), - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CSV_CHOSEN", Name = Path.GetFileName(result.Path) }, AppJsonContext.Default.UiResponse)); } + private void HandleChooseTemplate(JsonElement root) { var result = Dialog.FileOpen("png"); - if(!result.IsOk) return; + if (!result.IsOk) return; _options.TemplatePath = result.Path; - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "TEMPLATE_CHOSEN", - name = Path.GetFileName(result.Path), - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "TEMPLATE_CHOSEN", Name = Path.GetFileName(result.Path) }, AppJsonContext.Default.UiResponse)); } + private void HandleChooseOutput(JsonElement root) { var result = Dialog.FolderPicker(); - if(!result.IsOk) return; + if (!result.IsOk) return; _options.OutputDir = result.Path; - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "OUTPUT_CHOSEN", - name = Path.GetFileName(result.Path), - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "OUTPUT_CHOSEN", Name = Path.GetFileName(result.Path) }, AppJsonContext.Default.UiResponse)); } + private void HandleLoadConfig(JsonElement root) { - if(string.IsNullOrEmpty(_options.ConfigPath) || !File.Exists(_options.ConfigPath)) + if (string.IsNullOrEmpty(_options.ConfigPath) || !File.Exists(_options.ConfigPath)) { - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CONFIG_LOADED", - error = "config.json not found", - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CONFIG_LOADED", Error = "config.json not found" }, AppJsonContext.Default.UiResponse)); + return; } try { var config = ConfigurationLoader.LoadConfig(_options.ConfigPath); - if(config is null) + if (config is null) { - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CONFIG_LOADED", - error = "Failed to parse config.json", - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CONFIG_LOADED", Error = "Failed to parse config.json" }, AppJsonContext.Default.UiResponse)); return; } config.Remove("GLOBAL_SETTINGS"); - - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CONFIG_LOADED", - config = config, - templatePath = _options.TemplatePath, - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CONFIG_LOADED", Config = config, TemplatePath = _options.TemplatePath }, AppJsonContext.Default.UiResponse)); Console.WriteLine($"Sent to JS: {config.Count} fields"); } - catch(Exception ex) + catch (Exception ex) { Console.WriteLine($"LoadConfig error: {ex.Message}"); - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CONFIG_LOADED", - error = ex.Message, - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CONFIG_LOADED", Error = ex.Message }, AppJsonContext.Default.UiResponse)); } } + private void HandleChooseConfig(JsonElement root) { var result = Dialog.FileOpen("json"); if (!result.IsOk) return; _options.ConfigPath = result.Path; - Window?.SendWebMessage(JsonSerializer.Serialize(new - { - action = "CONFIG_CHOSEN", - name = Path.GetFileName(result.Path), - path = result.Path, - })); + Window?.SendWebMessage(JsonSerializer.Serialize( + new UiResponse { Action = "CONFIG_CHOSEN", Name = Path.GetFileName(result.Path), Path = result.Path }, AppJsonContext.Default.UiResponse)); } } -} \ No newline at end of file +} diff --git a/Services/Executor.cs b/Services/Executor.cs index 52053f1..5a7f82c 100644 --- a/Services/Executor.cs +++ b/Services/Executor.cs @@ -9,7 +9,7 @@ public class Executor { private readonly GenerationOptions _options; - public Action? OnMessageReady {get; set;} + public Action? OnMessageReady { get; set; } public Executor(GenerationOptions options) { @@ -64,11 +64,13 @@ public async Task ExecuteGenerationAsync(Dictionary config) Console.WriteLine($"⚡ Швидкість: {imagesPerSecond:F2} зображень/сек"); Console.WriteLine($"📝 Звіт: {_options.ReportPath}"); - OnMessageReady?.Invoke(JsonSerializer.Serialize(new { - action = "GENERATION_DONE", - ok = logs.Count(l => l.IsSuccess), - fail = logs.Count(l => !l.IsSuccess) - })); + OnMessageReady?.Invoke(JsonSerializer.Serialize( + new UiResponse + { + Action = "GENERATION_DONE", + Ok = ok, + Fail = fail + }, AppJsonContext.Default.UiResponse)); } catch (Exception ex) { @@ -76,4 +78,4 @@ public async Task ExecuteGenerationAsync(Dictionary config) } } } -} \ No newline at end of file +} diff --git a/Utils/ConfigurationLoader.cs b/Utils/ConfigurationLoader.cs index 8518636..25ccb99 100644 --- a/Utils/ConfigurationLoader.cs +++ b/Utils/ConfigurationLoader.cs @@ -9,11 +9,12 @@ public static class ConfigurationLoader { public static Dictionary? LoadConfig(string path) { - // Your exact existing code goes here... + if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; try { string json = File.ReadAllText(path, Encoding.UTF8); - var result = JsonSerializer.Deserialize>(json); + // ВИКОРИСТОВУЄМО AOT КОНТЕКСТ + var result = JsonSerializer.Deserialize(json, AppJsonContext.Default.DictionaryStringFieldConfig); if (result is null) Console.WriteLine("❌ config.json порожній або невалідний."); return result; @@ -27,13 +28,34 @@ public static class ConfigurationLoader public static List>? LoadCsv(string path) { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; try { + var csvConfig = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.InvariantCulture) + { + MissingFieldFound = null, // Відсутнє поле = null замість помилки + BadDataFound = null // Не падати через дрібні помилки форматування + }; + using var reader = new StreamReader(path, Encoding.UTF8); - using var csv = new CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture); - return csv.GetRecords() - .Select(r => (IDictionary)r) - .ToList(); + using var csv = new CsvHelper.CsvReader(reader, csvConfig); + + var records = new List>(); + + if (csv.Read() && csv.ReadHeader() && csv.HeaderRecord != null) + { + string[] headers = csv.HeaderRecord; + while (csv.Read()) + { + var dict = new Dictionary(); + foreach (var header in headers) + { + dict[header] = csv.GetField(header) ?? string.Empty; + } + records.Add(dict); + } + } + return records; } catch (Exception ex) { @@ -41,13 +63,13 @@ public static class ConfigurationLoader return null; } } + public static void SaveConfig(string path, Dictionary config) { try { - // WriteIndented makes the JSON pretty and readable - var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; - string jsonString = JsonSerializer.Serialize(config, jsonOptions); + // ВИКОРИСТОВУЄМО AOT КОНТЕКСТ (Він вже має WriteIndented = true з атрибута) + string jsonString = JsonSerializer.Serialize(config, AppJsonContext.Default.DictionaryStringFieldConfig); File.WriteAllText(path, jsonString, Encoding.UTF8); Console.WriteLine("💾 config.json successfully overwritten with new UI coordinates."); From 7526eaed8568daebfd2c6752d2ea39f58f85ab07 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 17 Jun 2026 14:34:38 +0200 Subject: [PATCH 3/4] add the option for native --- AvifGenerator.csproj | 8 ++++++++ README.md | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/AvifGenerator.csproj b/AvifGenerator.csproj index 152f3f7..6528177 100644 --- a/AvifGenerator.csproj +++ b/AvifGenerator.csproj @@ -8,6 +8,14 @@ enable + + Exe + Size + true + false + true + + diff --git a/README.md b/README.md index e0cf16c..d9dcd8a 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile= ``` + **For Windows (With console window for debugging):** ```bash @@ -61,6 +62,12 @@ dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile= ``` +and with native performance use: + +```bash +dotnet publish -c AOT -r win-x64 +``` + **For Linux:** ```bash @@ -68,6 +75,12 @@ dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFil ``` +and with native performance use: + +```bash +dotnet publish -c AOT -r linux-x64 +``` + ## 📖 How to Use 1. **Launch the App:** Open the compiled executable. From 4229c206f9cd06879207e1b711005e325587cc00 Mon Sep 17 00:00:00 2001 From: Mark <138571922+RoNiN-shadow@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:49:49 +0300 Subject: [PATCH 4/4] increase cpu --- Services/Executor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/Executor.cs b/Services/Executor.cs index 5a7f82c..00e0356 100644 --- a/Services/Executor.cs +++ b/Services/Executor.cs @@ -28,7 +28,7 @@ public async Task ExecuteGenerationAsync(Dictionary config) Console.WriteLine($"📄 Записів у CSV: {records.Count}"); int totalCores = Environment.ProcessorCount; - int maxConcurrentImages = Math.Max(1, totalCores / 4); + int maxConcurrentImages = Math.Max(1, totalCores / 2); int threadsPerImage = Math.Max(1, totalCores / maxConcurrentImages); Console.WriteLine($"\n⏳ Аналіз шаблону: {Path.GetFileName(_options.TemplatePath)}...");