瀏覽代碼

Merge pull request #869 from PixiEditor/fixes/27.03.2025

Fixes/27.03.2025
Krzysztof Krysiński 4 月之前
父節點
當前提交
f2ffb4fa43
共有 29 個文件被更改,包括 302 次插入105 次删除
  1. 0 1
      src/ChunkyImageLib/ChunkyImageLib.csproj
  2. 1 1
      src/Drawie
  3. 1 1
      src/PixiDocks
  4. 3 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/PreferencesConstants.cs
  5. 5 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs
  6. 5 0
      src/PixiEditor.SVG/StyleContext.cs
  7. 1 1
      src/PixiEditor.SVG/SvgElement.cs
  8. 1 0
      src/PixiEditor.SVG/SvgProperty.cs
  9. 10 2
      src/PixiEditor/Data/Localization/Languages/en.json
  10. 31 11
      src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs
  11. 1 1
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveBackupJob.cs
  12. 5 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  13. 18 18
      src/PixiEditor/Models/Files/ImageFileType.cs
  14. 8 8
      src/PixiEditor/Models/Files/PixiFileType.cs
  15. 2 2
      src/PixiEditor/Models/Files/SvgFileType.cs
  16. 4 4
      src/PixiEditor/Models/Files/VideoFileType.cs
  17. 1 0
      src/PixiEditor/Models/Handlers/ITransformHandler.cs
  18. 106 5
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  19. 26 23
      src/PixiEditor/Models/IO/Exporter.cs
  20. 2 0
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  21. 18 1
      src/PixiEditor/ViewModels/Dock/LayoutManager.cs
  22. 1 1
      src/PixiEditor/ViewModels/Document/AutosaveDocumentViewModel.cs
  23. 7 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  24. 18 13
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  25. 3 0
      src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs
  26. 7 7
      src/PixiEditor/ViewModels/UserPreferences/Settings/GeneralSettings.cs
  27. 0 1
      src/PixiEditor/ViewModels/ViewModelMain.cs
  28. 3 0
      src/PixiEditor/Views/Rendering/Scene.cs
  29. 14 3
      src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

+ 0 - 1
src/ChunkyImageLib/ChunkyImageLib.csproj

@@ -13,7 +13,6 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="WriteableBitmapEx" Version="1.6.8" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 7e4de2359281d40b99049748d7056f6cd836fbde
+Subproject commit 4037b56fc0accf094d10fd445f7ff93ed23131f5

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 71c3120eda7a644c7da88885504a9e30de68161d
+Subproject commit d4e89ae9349cb7329eb98866541893e1a8510094

+ 3 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/PreferencesConstants.cs

@@ -30,4 +30,7 @@ public static class PreferencesConstants
 
     public const string OpenDirectoryOnExport = "OpenDirectoryOnExport";
     public const bool OpenDirectoryOnExportDefault = true;
+
+    public const string AnalyticsEnabled = "AnalyticsEnabled";
+    public const bool AnalyticsEnabledDefault = true;
 }

+ 5 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs

@@ -64,4 +64,9 @@ public static class PixiEditorSettings
 
         public static SyncedSetting<bool> ShowLayerCount { get; } = SyncedSetting.NonOwned(PixiEditor, true);
     }
+
+    public static class Analytics
+    {
+        public static SyncedSetting<bool> AnalyticsEnabled { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+    }
 }

+ 5 - 0
src/PixiEditor.SVG/StyleContext.cs

@@ -20,6 +20,7 @@ public struct StyleContext
     public SvgProperty<SvgNumericUnit> Opacity { get; }
     public SvgProperty<SvgStyleUnit> InlineStyle { get; set; }
     public VecD ViewboxOrigin { get; set; }
+    public VecD ViewboxSize { get; set; }
     public SvgDefs Defs { get; set; }
 
     public StyleContext()
@@ -50,6 +51,9 @@ public struct StyleContext
         ViewboxOrigin = new VecD(
             document.ViewBox.Unit.HasValue ? -document.ViewBox.Unit.Value.Value.X : 0,
             document.ViewBox.Unit.HasValue ? -document.ViewBox.Unit.Value.Value.Y : 0);
+        ViewboxSize = new VecD(
+            document.ViewBox.Unit.HasValue ? document.ViewBox.Unit.Value.Value.Width : 0,
+            document.ViewBox.Unit.HasValue ? document.ViewBox.Unit.Value.Value.Height : 0);
         InlineStyle = document.Style;
         Defs = document.Defs;
     }
@@ -159,6 +163,7 @@ public struct StyleContext
         }
 
         styleContext.ViewboxOrigin = ViewboxOrigin;
+        styleContext.ViewboxSize = ViewboxSize;
 
         if (InlineStyle.Unit != null)
         {

+ 1 - 1
src/PixiEditor.SVG/SvgElement.cs

@@ -103,7 +103,7 @@ public class SvgElement(string tagName)
         do
         {
             SvgProperty matchingProperty = properties.FirstOrDefault(x =>
-                string.Equals(x.SvgName, reader.Name, StringComparison.OrdinalIgnoreCase));
+                string.Equals(x.SvgFullName, reader.Name, StringComparison.OrdinalIgnoreCase));
             if (matchingProperty != null)
             {
                 ParseAttribute(matchingProperty, reader, defs);

+ 1 - 0
src/PixiEditor.SVG/SvgProperty.cs

@@ -18,6 +18,7 @@ public abstract class SvgProperty
     public string? NamespaceName { get; set; }
     public string SvgName { get; set; }
     public ISvgUnit? Unit { get; set; }
+    public string? SvgFullName => NamespaceName == null ? SvgName : $"{NamespaceName}:{SvgName}";
 
     public ISvgUnit? CreateDefaultUnit()
     {

+ 10 - 2
src/PixiEditor/Data/Localization/Languages/en.json

@@ -9,7 +9,6 @@
   "KEY_BINDINGS": "Key Bindings",
   "MISC": "Misc",
   "SHOW_STARTUP_WINDOW": "Show Startup Window",
-  "SHOW_IMAGE_PREVIEW_TASKBAR": "Show image preview in taskbar",
   "RECENT_FILE_LENGTH": "Recent file list length",
   "RECENT_FILE_LENGTH_TOOLTIP": "How many documents are shown under File > Recent. Default: 8",
   "DEFAULT_NEW_SIZE": "Default new file size",
@@ -981,5 +980,14 @@
   "CLAMP_TILE_MODE": "Clamp",
   "REPEAT_TILE_MODE": "Repeat",
   "MIRROR_TILE_MODE": "Mirror",
-  "DECAL_TILE_MODE": "Decal"
+  "DECAL_TILE_MODE": "Decal",
+  "ERR_UNKNOWN_FILE_FORMAT": "Unknown file format",
+  "ERR_EXPORT_SIZE_INVALID": "Invalid export size. Values must be greater than 0.",
+  "ERR_UNKNOWN_IMG_FORMAT": "Unknown image format '{0}'.",
+  "ERR_FAILED_GENERATE_SPRITE_SHEET": "Failed generating sprite sheet",
+  "ERR_NO_RENDERER": "Animation renderer not found.",
+  "ERR_RENDERING_FAILED": "Rendering failed",
+  "ENABLE_ANALYTICS": "Send anonymous analytics",
+  "ANALYTICS_INFO": "We collect anonymous usage data to improve PixiEditor. No personal data is collected.",
+  "LANGUAGE_INFO": "All translations are community-driven. Join our Discord server for more information."
 }

+ 31 - 11
src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs

@@ -1,4 +1,7 @@
-using PixiEditor.Helpers;
+using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
+using PixiEditor.Helpers;
 
 namespace PixiEditor.Models.AnalyticsAPI;
 
@@ -6,11 +9,11 @@ public class AnalyticsPeriodicReporter
 {
     private int _sendExceptions = 0;
     private bool _resumeSession;
-    
+
     private readonly SemaphoreSlim _semaphore = new(1, 1);
     private readonly AnalyticsClient _client;
     private readonly PeriodicPerformanceReporter _performanceReporter;
-    
+
     private readonly List<AnalyticEvent> _backlog = new();
     private readonly CancellationTokenSource _cancellationToken = new();
 
@@ -19,25 +22,30 @@ public class AnalyticsPeriodicReporter
     public static AnalyticsPeriodicReporter? Instance { get; private set; }
 
     public Guid SessionId { get; private set; }
-    
+
     public AnalyticsPeriodicReporter(AnalyticsClient client)
     {
         if (Instance != null)
             throw new InvalidOperationException("There's already a AnalyticsReporter present");
 
         Instance = this;
-        
+
         _client = client;
         _performanceReporter = new PeriodicPerformanceReporter(this);
+
+        PixiEditorSettings.Analytics.AnalyticsEnabled.ValueChanged += EnableAnalyticsOnValueChanged;
     }
 
     public void Start(Guid? sessionId)
     {
+        if (!PixiEditorSettings.Analytics.AnalyticsEnabled.Value)
+            return;
+
         if (sessionId != null)
         {
             SessionId = sessionId.Value;
             _resumeSession = true;
-            
+
             _backlog.Add(new AnalyticEvent { Time = DateTime.UtcNow, EventType = AnalyticEventTypes.ResumeSession });
         }
 
@@ -47,7 +55,7 @@ public class AnalyticsPeriodicReporter
 
     public async Task StopAsync()
     {
-        _cancellationToken.Cancel();
+        await _cancellationToken.CancelAsync();
 
         await _client.EndSessionAsync(SessionId).WaitAsync(TimeSpan.FromSeconds(1));
     }
@@ -59,7 +67,7 @@ public class AnalyticsPeriodicReporter
         {
             return;
         }
-        
+
         Task.Run(() =>
         {
             _semaphore.Wait();
@@ -97,7 +105,7 @@ public class AnalyticsPeriodicReporter
             {
                 if (_backlog.Any(x => x.ExpectingEndTimeReport))
                     WaitForEndTimes();
-                
+
                 await SendBacklogAsync();
 
                 await Task.Delay(TimeSpan.FromSeconds(10));
@@ -137,7 +145,7 @@ public class AnalyticsPeriodicReporter
             {
                 return;
             }
-            
+
             var result = await _client.SendEventsAsync(SessionId, _backlog, _cancellationToken.Token);
             _backlog.Clear();
 
@@ -184,7 +192,7 @@ public class AnalyticsPeriodicReporter
 
         if (!result)
         {
-            _cancellationToken.Cancel();
+            await _cancellationToken.CancelAsync();
         }
     }
 
@@ -196,4 +204,16 @@ public class AnalyticsPeriodicReporter
             _sendExceptions++;
         }
     }
+
+    private void EnableAnalyticsOnValueChanged(Setting<bool> setting, bool enabled)
+    {
+        if (enabled)
+        {
+            Start(null);
+        }
+        else
+        {
+            StopAsync();
+        }
+    }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveBackupJob.cs

@@ -56,7 +56,7 @@ internal class AutosaverSaveBackupJob(DocumentViewModel documentToSave, int back
             ExportConfig config = new ExportConfig(documentToSave.SizeBindable);
             var result = Exporter.TrySave(documentToSave, filePath, config, null);
 
-            if (result == SaveResult.Success)
+            if (result.ResultType == SaveResultType.Success)
             {
                 documentToSave.MarkAsAutosaved();
                 documentToSave.AutosaveViewModel.AddAutosaveHistoryEntry(AutosaveHistoryType.Periodic,

+ 5 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -124,6 +124,11 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
         document.Operations.InvokeCustomAction(
             () =>
             {
+                if (!document.TextOverlayHandler.IsActive)
+                {
+                    document.TextOverlayHandler.Show(lastText, position, toolbar.ConstructFont(), lastMatrix,
+                        toolbar.Spacing);
+                }
                 document.TextOverlayHandler.SetCursorPosition(args.PositionOnCanvas);
             }, false);
     }

+ 18 - 18
src/PixiEditor/Models/Files/ImageFileType.cs

@@ -28,7 +28,7 @@ internal abstract class ImageFileType : IoFileType
             job?.Report(0, new LocalizedString("GENERATING_SPRITE_SHEET"));
             finalSurface = GenerateSpriteSheet(document, exportConfig, job);
             if (finalSurface == null)
-                return SaveResult.UnknownError;
+                return new SaveResult(SaveResultType.CustomError, "ERR_FAILED_GENERATE_SPRITE_SHEET");
         }
         else
         {
@@ -37,12 +37,12 @@ internal abstract class ImageFileType : IoFileType
             var exportSize = exportConfig.ExportSize;
             if (exportSize.X <= 0 || exportSize.Y <= 0)
             {
-                return SaveResult.UnknownError; // TODO: Add InvalidParameters error type
+                return new SaveResult(SaveResultType.CustomError, "ERR_EXPORT_SIZE_INVALID");
             }
 
             var maybeBitmap = document.TryRenderWholeImage(0, exportSize);
             if (maybeBitmap.IsT0)
-                return SaveResult.ConcurrencyError;
+                return new SaveResult(SaveResultType.ConcurrencyError);
 
             finalSurface = maybeBitmap.AsT1;
         }
@@ -51,7 +51,7 @@ internal abstract class ImageFileType : IoFileType
 
         if (mappedFormat == EncodedImageFormat.Unknown)
         {
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.CustomError, new LocalizedString("ERR_UNKNOWN_IMG_FORMAT", EncodedImageFormat));
         }
 
         UniversalFileEncoder encoder = new(mappedFormat);
@@ -72,7 +72,7 @@ internal abstract class ImageFileType : IoFileType
             job?.Report(0, new LocalizedString("GENERATING_SPRITE_SHEET"));
             finalSurface = GenerateSpriteSheet(document, config, job);
             if (finalSurface == null)
-                return SaveResult.UnknownError;
+                return new SaveResult(SaveResultType.CustomError, "ERR_FAILED_GENERATE_SPRITE_SHEET");
         }
         else
         {
@@ -81,12 +81,12 @@ internal abstract class ImageFileType : IoFileType
             var exportSize = config.ExportSize;
             if (exportSize.X <= 0 || exportSize.Y <= 0)
             {
-                return SaveResult.UnknownError; // TODO: Add InvalidParameters error type
+                return new SaveResult(SaveResultType.CustomError, "ERR_EXPORT_SIZE_INVALID");
             }
 
             var maybeBitmap = document.TryRenderWholeImage(0, exportSize);
             if (maybeBitmap.IsT0)
-                return SaveResult.ConcurrencyError;
+                return new SaveResult(SaveResultType.ConcurrencyError);
 
             finalSurface = maybeBitmap.AsT1;
         }
@@ -95,7 +95,7 @@ internal abstract class ImageFileType : IoFileType
 
         if (mappedFormat == EncodedImageFormat.Unknown)
         {
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.CustomError, new LocalizedString("ERR_UNKNOWN_IMG_FORMAT", EncodedImageFormat));
         }
 
         UniversalFileEncoder encoder = new(mappedFormat);
@@ -160,22 +160,22 @@ internal abstract class ImageFileType : IoFileType
         }
         catch (SecurityException)
         {
-            return SaveResult.SecurityError;
+            return new SaveResult(SaveResultType.SecurityError);
         }
         catch (UnauthorizedAccessException)
         {
-            return SaveResult.SecurityError;
+            return new SaveResult(SaveResultType.SecurityError);
         }
         catch (IOException)
         {
-            return SaveResult.IoError;
+            return new SaveResult(SaveResultType.IoError);
         }
         catch
         {
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.UnknownError);
         }
 
-        return SaveResult.Success;
+        return new SaveResult(SaveResultType.Success);
     }
 
     /// <summary>
@@ -193,21 +193,21 @@ internal abstract class ImageFileType : IoFileType
         }
         catch (SecurityException)
         {
-            return SaveResult.SecurityError;
+            return new SaveResult(SaveResultType.SecurityError);
         }
         catch (UnauthorizedAccessException)
         {
-            return SaveResult.SecurityError;
+            return new SaveResult(SaveResultType.SecurityError);
         }
         catch (IOException)
         {
-            return SaveResult.IoError;
+            return new SaveResult(SaveResultType.IoError);
         }
         catch
         {
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.UnknownError);
         }
 
-        return SaveResult.Success;
+        return new SaveResult(SaveResultType.Success);
     }
 }

+ 8 - 8
src/PixiEditor/Models/Files/PixiFileType.cs

@@ -29,19 +29,19 @@ internal class PixiFileType : IoFileType
         }
         catch (UnauthorizedAccessException e)
         {
-            return SaveResult.SecurityError;
+            return new SaveResult(SaveResultType.SecurityError);
         }
         catch (IOException)
         {
-            return SaveResult.IoError;
+            return new SaveResult(SaveResultType.IoError);
         }
         catch (Exception e)
         {
             CrashHelper.SendExceptionInfo(e);
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.UnknownError);
         }
 
-        return SaveResult.Success;
+        return new SaveResult(SaveResultType.Success);
     }
 
     public override SaveResult TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config,
@@ -55,18 +55,18 @@ internal class PixiFileType : IoFileType
         }
         catch (UnauthorizedAccessException e)
         {
-            return SaveResult.SecurityError;
+            return new SaveResult(SaveResultType.SecurityError);
         }
         catch (IOException)
         {
-            return SaveResult.IoError;
+            return new SaveResult(SaveResultType.IoError);
         }
         catch (Exception e)
         {
             CrashHelper.SendExceptionInfo(e);
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.UnknownError);
         }
 
-        return SaveResult.Success;
+        return new SaveResult(SaveResultType.Success);
     }
 }

+ 2 - 2
src/PixiEditor/Models/Files/SvgFileType.cs

@@ -35,7 +35,7 @@ internal class SvgFileType : IoFileType
         await writer.WriteAsync(xml);
 
         job?.Report(1, string.Empty);
-        return SaveResult.Success;
+        return new SaveResult(SaveResultType.Success);
     }
 
     public override SaveResult TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config,
@@ -55,6 +55,6 @@ internal class SvgFileType : IoFileType
         writer.Write(xml);
 
         job?.Report(1, string.Empty);
-        return SaveResult.Success;
+        return new SaveResult(SaveResultType.Success);
     }
 }

+ 4 - 4
src/PixiEditor/Models/Files/VideoFileType.cs

@@ -14,7 +14,7 @@ internal abstract class VideoFileType : IoFileType
         ExportConfig config, ExportJob? job)
     {
         if (config.AnimationRenderer is null)
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.CustomError, new LocalizedString("ERR_NO_RENDERER"));
 
         List<Image> frames = new();
 
@@ -51,14 +51,14 @@ internal abstract class VideoFileType : IoFileType
             frame.Dispose();
         }
 
-        return result ? SaveResult.Success : SaveResult.UnknownError;
+        return result ? new SaveResult(SaveResultType.Success) : new SaveResult(SaveResultType.CustomError, new LocalizedString("ERR_RENDERING_FAILED"));
     }
 
     public override SaveResult TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config,
         ExportJob? job)
     {
         if (config.AnimationRenderer is null)
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.CustomError, new LocalizedString("ERR_NO_RENDERER"));
 
         List<Image> frames = new();
 
@@ -95,6 +95,6 @@ internal abstract class VideoFileType : IoFileType
             frame.Dispose();
         }
 
-        return result ? SaveResult.Success : SaveResult.UnknownError;
+        return result ? new SaveResult(SaveResultType.Success) : new SaveResult(SaveResultType.CustomError, new LocalizedString("ERR_RENDERING_FAILED"));
     }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/ITransformHandler.cs

@@ -23,4 +23,5 @@ internal interface ITransformHandler : IHandler
     public bool ShowHandles { get; set; }
     public bool IsSizeBoxEnabled { get; set; }
     public bool CanAlignToPixels { get; set; }
+    public bool TransformActive { get; }
 }

+ 106 - 5
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -1,6 +1,8 @@
 using System.Diagnostics.CodeAnalysis;
+using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
@@ -29,7 +31,7 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         string xml = File.ReadAllText(path);
         SvgDocument document = SvgDocument.Parse(xml);
 
-        if(document == null)
+        if (document == null)
         {
             throw new SvgParsingException("Failed to parse SVG document");
         }
@@ -58,6 +60,10 @@ internal class SvgDocumentBuilder : IDocumentBuilder
                     {
                         lastId = AddGroup(group, graph, style, lastId);
                     }
+                    else if (element is SvgImage svgImage)
+                    {
+                        lastId = AddImage(svgImage, style, graph, lastId);
+                    }
                 }
 
                 graph.WithOutputNode(lastId, "Output");
@@ -103,7 +109,10 @@ internal class SvgDocumentBuilder : IDocumentBuilder
 
         NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<VectorLayerNode>(out int id)
             .WithName(name)
-            .WithInputValues(new Dictionary<string, object>() { { StructureNode.OpacityPropertyName, (float)(styleContext.Opacity.Unit?.Value ?? 1f) } })
+            .WithInputValues(new Dictionary<string, object>()
+            {
+                { StructureNode.OpacityPropertyName, (float)(styleContext.Opacity.Unit?.Value ?? 1f) }
+            })
             .WithAdditionalData(new Dictionary<string, object>() { { "ShapeData", shapeData } });
 
         if (lastId != null)
@@ -137,6 +146,10 @@ internal class SvgDocumentBuilder : IDocumentBuilder
             {
                 childId = AddGroup(childGroup, graph, childStyle, childId, connectTo);
             }
+            else if (child is SvgImage image)
+            {
+                childId = AddImage(image, childStyle, graph, childId);
+            }
         }
 
         NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<FolderNode>(out int id)
@@ -173,6 +186,91 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         return lastId;
     }
 
+    private int? AddImage(SvgImage image, StyleContext style, NodeGraphBuilder graph, int? lastId)
+    {
+        byte[] bytes = TryReadImage(image.Href.Unit?.Value ?? "");
+
+        Surface? imgSurface = bytes is { Length: > 0 } ? Surface.Load(bytes) : null;
+        Surface? finalSurface = null;
+
+        if (imgSurface != null)
+        {
+            if (imgSurface.Size.X != (int)image.Width.Unit?.PixelsValue ||
+                imgSurface.Size.Y != (int)image.Height.Unit?.PixelsValue)
+            {
+                var resized = imgSurface.ResizeNearestNeighbor(
+                    new VecI((int)image.Width.Unit?.PixelsValue, (int)image.Height.Unit?.PixelsValue));
+                imgSurface.Dispose();
+                imgSurface = resized;
+            }
+        }
+
+        if (style.ViewboxSize.ShortestAxis > 0 && imgSurface != null)
+        {
+            finalSurface = new Surface((VecI)style.ViewboxSize);
+            double x = image.X.Unit?.PixelsValue ?? 0;
+            double y = image.Y.Unit?.PixelsValue ?? 0;
+            finalSurface.DrawingSurface.Canvas.DrawSurface(imgSurface.DrawingSurface, (int)x, (int)y);
+            imgSurface.Dispose();
+        }
+
+        var graphBuilder = graph.WithImageLayerNode(
+            image.Id.Unit?.Value ?? new LocalizedString("NEW_LAYER").Value,
+            finalSurface, ColorSpace.CreateSrgb(), out int id);
+
+        if (lastId != null)
+        {
+            var nodeBuilder = graphBuilder.AllNodes[^1];
+
+            Dictionary<string, object> inputValues = new()
+            {
+                { StructureNode.OpacityPropertyName, (float)(style.Opacity.Unit?.Value ?? 1f) }
+            };
+
+            nodeBuilder.WithInputValues(inputValues);
+            nodeBuilder.WithConnections([
+                new PropertyConnection()
+                {
+                    InputPropertyName = "Background", OutputPropertyName = "Output", OutputNodeId = lastId.Value
+                }
+            ]);
+        }
+
+        lastId = id;
+
+        return lastId;
+    }
+
+    private byte[] TryReadImage(string svgHref)
+    {
+        if (string.IsNullOrEmpty(svgHref))
+        {
+            return [];
+        }
+
+        if (svgHref.StartsWith("data:image/png;base64,"))
+        {
+            return Convert.FromBase64String(svgHref.Replace("data:image/png;base64,", ""));
+        }
+
+        // TODO: Implement downloading images from the internet
+        /*if (Uri.TryCreate(svgHref, UriKind.Absolute, out Uri? uri))
+        {
+            try
+            {
+                using WebClient client = new();
+                return client.DownloadData(uri);
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+                return [];
+            }
+        }*/
+
+        return [];
+    }
+
     private EllipseVectorData AddEllipse(SvgElement element)
     {
         if (element is SvgCircle circle)
@@ -242,9 +340,11 @@ internal class SvgDocumentBuilder : IDocumentBuilder
 
     private TextVectorData AddText(SvgText element)
     {
-        Font font = element.FontFamily.Unit.HasValue ? Font.FromFamilyName(element.FontFamily.Unit.Value.Value) : Font.CreateDefault();
+        Font font = element.FontFamily.Unit.HasValue
+            ? Font.FromFamilyName(element.FontFamily.Unit.Value.Value)
+            : Font.CreateDefault();
         FontFamilyName? missingFont = null;
-        if(font == null)
+        if (font == null)
         {
             font = Font.CreateDefault();
             missingFont = new FontFamilyName(element.FontFamily.Unit.Value.Value);
@@ -273,7 +373,8 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         }
 
         bool hasFill = styleContext.Fill.Unit?.Paintable is { AnythingVisible: true };
-        bool hasStroke = styleContext.Stroke.Unit?.Paintable is { AnythingVisible: true } || styleContext.StrokeWidth.Unit is { PixelsValue: > 0 };
+        bool hasStroke = styleContext.Stroke.Unit?.Paintable is { AnythingVisible: true } ||
+                         styleContext.StrokeWidth.Unit is { PixelsValue: > 0 };
         bool hasTransform = styleContext.Transform.Unit is { MatrixValue.IsIdentity: false };
 
         shapeData.Fill = hasFill;

+ 26 - 23
src/PixiEditor/Models/IO/Exporter.cs

@@ -13,33 +13,36 @@ using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Models.IO;
 
-internal enum DialogSaveResult
+internal enum SaveResultType
 {
     Success = 0,
     InvalidPath = 1,
     ConcurrencyError = 2,
     SecurityError = 3,
     IoError = 4,
-    UnknownError = 5,
-    Cancelled = 6,
+    CustomError = 5,
+    UnknownError = 6,
+    Cancelled = 7,
 }
 
-internal enum SaveResult
+internal class SaveResult
 {
-    Success = 0,
-    InvalidPath = 1,
-    ConcurrencyError = 2,
-    SecurityError = 3,
-    IoError = 4,
-    UnknownError = 5,
+    public SaveResultType ResultType { get; set; }
+    public string? ErrorMessage { get; set; }
+
+    public SaveResult(SaveResultType resultType, string? errorMessage = null)
+    {
+        ResultType = resultType;
+        ErrorMessage = errorMessage;
+    }
 }
 
 internal class ExporterResult
 {
-    public DialogSaveResult Result { get; set; }
+    public SaveResult Result { get; set; }
     public string Path { get; set; }
 
-    public ExporterResult(DialogSaveResult result, string path)
+    public ExporterResult(SaveResult result, string path)
     {
         Result = result;
         Path = path;
@@ -54,7 +57,7 @@ internal class Exporter
     public static async Task<ExporterResult> TrySaveWithDialog(DocumentViewModel document, ExportConfig exportConfig,
         ExportJob? job)
     {
-        ExporterResult result = new(DialogSaveResult.UnknownError, null);
+        ExporterResult result = new(new SaveResult(SaveResultType.UnknownError), null);
 
         if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {
@@ -67,7 +70,7 @@ internal class Exporter
 
             if (file is null)
             {
-                result.Result = DialogSaveResult.Cancelled;
+                result.Result.ResultType = SaveResultType.Cancelled;
                 return result;
             }
 
@@ -75,12 +78,12 @@ internal class Exporter
 
             (SaveResult Result, string finalPath) saveResult =
                 await TrySaveUsingDataFromDialog(document, file.Path.LocalPath, fileType, exportConfig, job);
-            if (saveResult.Result == SaveResult.Success)
+            if (saveResult.Result.ResultType == SaveResultType.Success)
             {
                 result.Path = saveResult.finalPath;
             }
 
-            result.Result = (DialogSaveResult)saveResult.Result;
+            result.Result = saveResult.Result;
         }
 
         return result;
@@ -95,7 +98,7 @@ internal class Exporter
     {
         string finalPath = SupportedFilesHelper.FixFileExtension(pathFromDialog, fileTypeFromDialog);
         var saveResult = await TrySaveAsync(document, finalPath, exportConfig, job);
-        if (saveResult != SaveResult.Success)
+        if (saveResult.ResultType != SaveResultType.Success)
             finalPath = "";
 
         return (saveResult, finalPath);
@@ -109,12 +112,12 @@ internal class Exporter
     {
         string directory = Path.GetDirectoryName(pathWithExtension);
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
-            return SaveResult.InvalidPath;
+            return new SaveResult(SaveResultType.InvalidPath);
 
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
 
         if (typeFromPath is null)
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.CustomError, "ERR_UNKNOWN_FILE_FORMAT");
 
         try
         {
@@ -127,7 +130,7 @@ internal class Exporter
             job?.Finish();
             Console.WriteLine(e);
             CrashHelper.SendExceptionInfo(e);
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.UnknownError);
         }
     }
 
@@ -136,12 +139,12 @@ internal class Exporter
     {
         string directory = Path.GetDirectoryName(pathWithExtension);
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
-            return SaveResult.InvalidPath;
+            return new SaveResult(SaveResultType.InvalidPath);
 
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
 
         if (typeFromPath is null)
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.CustomError, "ERR_UNKNOWN_FILE_FORMAT");
 
         try
         {
@@ -154,7 +157,7 @@ internal class Exporter
             job?.Finish();
             Console.WriteLine(e);
             CrashHelper.SendExceptionInfo(e);
-            return SaveResult.UnknownError;
+            return new SaveResult(SaveResultType.UnknownError);
         }
     }
 

+ 2 - 0
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -2,11 +2,13 @@
 using Avalonia.Threading;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Rendering;
 

+ 18 - 1
src/PixiEditor/ViewModels/Dock/LayoutManager.cs

@@ -101,7 +101,8 @@ internal class LayoutManager
                     SplitDirection = DockingDirection.Bottom,
                     Second = new DockableArea
                     {
-                        Id = "DocumentPreviewArea", ActiveDockable = DockContext.CreateDockable(documentPreviewDockViewModel)
+                        Id = "DocumentPreviewArea",
+                        ActiveDockable = DockContext.CreateDockable(documentPreviewDockViewModel)
                     }
                 }
             }
@@ -155,6 +156,22 @@ internal class LayoutManager
         }
     }
 
+    public void ShowViewport(ViewportWindowViewModel viewport)
+    {
+        foreach (var element in ActiveLayout.Root)
+        {
+            if (element is IDockableHost dockableHost)
+            {
+                var dockable = dockableHost.Dockables.FirstOrDefault(x => x.Id == viewport.Id);
+                if (dockable != null)
+                {
+                    dockableHost.ActiveDockable = dockable;
+                    return;
+                }
+            }
+        }
+    }
+
     private DockableArea? TryFindArea(string name)
     {
         DockableArea? result = null;

+ 1 - 1
src/PixiEditor/ViewModels/Document/AutosaveDocumentViewModel.cs

@@ -99,7 +99,7 @@ internal class AutosaveDocumentViewModel : ObservableObject
             string filePath = AutosavePath;
             Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
             ExportConfig config = new ExportConfig(Document.SizeBindable);
-            bool success = Exporter.TrySave(Document, filePath, config, null) == SaveResult.Success;
+            bool success = Exporter.TrySave(Document, filePath, config, null).ResultType == SaveResultType.Success;
             if (success)
             {
                 AddAutosaveHistoryEntry(type, AutosaveHistoryResult.SavedBackup);

+ 7 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -282,6 +282,7 @@ internal partial class DocumentViewModel
         });
 
         var image = CreateImageElement(resizeFactor, tightBounds.Value, toSave, useNearestNeighborForImageUpscaling);
+        image.Id.Unit = new SvgStringUnit(member.NodeNameBindable);
 
         elementContainer.Children.Add(image);
     }
@@ -325,7 +326,8 @@ internal partial class DocumentViewModel
         text.FontStyle.Unit = new SvgEnumUnit<SvgFontStyle>(font.Italic ? SvgFontStyle.Italic : SvgFontStyle.Normal);
         text.Stroke.Unit = new SvgPaintServerUnit(textData.Stroke);
         text.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(textData.StrokeWidth);
-        text.Fill.Unit = new SvgPaintServerUnit(textData.Fill ? textData.FillPaintable : new ColorPaintable(Colors.Transparent));
+        text.Fill.Unit =
+            new SvgPaintServerUnit(textData.Fill ? textData.FillPaintable : new ColorPaintable(Colors.Transparent));
 
         return text;
     }
@@ -540,6 +542,8 @@ internal partial class DocumentViewModel
         {
             if (keyFrame is IKeyFrameChildrenContainer container)
             {
+                if (!nodeIdMap.ContainsKey(keyFrame.NodeId)) continue;
+
                 KeyFrameGroup group = new();
                 group.NodeId = nodeIdMap[keyFrame.NodeId];
                 group.Enabled = keyFrame.IsVisible;
@@ -548,6 +552,8 @@ internal partial class DocumentViewModel
                 {
                     if (child is IReadOnlyRasterKeyFrame rasterKeyFrame)
                     {
+                        if (!nodeIdMap.ContainsKey(rasterKeyFrame.NodeId)) continue;
+
                         BuildRasterKeyFrame(rasterKeyFrame, graph, group, nodeIdMap, keyFrameIds);
                     }
                 }

+ 18 - 13
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -535,9 +535,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
             ExportConfig config = new ExportConfig(document.SizeBindable);
             var result = await Exporter.TrySaveWithDialog(document, config, null);
-            if (result.Result == DialogSaveResult.Cancelled)
+            if (result.Result.ResultType == SaveResultType.Cancelled)
                 return false;
-            if (result.Result != DialogSaveResult.Success)
+            if (result.Result.ResultType != SaveResultType.Success)
             {
                 ShowSaveError(result.Result);
                 return false;
@@ -550,9 +550,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
             ExportConfig config = new ExportConfig(document.SizeBindable);
             var result = await Exporter.TrySaveAsync(document, document.FullFilePath, config, null);
-            if (result != SaveResult.Success)
+            if (result.ResultType != SaveResultType.Success)
             {
-                ShowSaveError((DialogSaveResult)result);
+                ShowSaveError(result);
                 return false;
             }
 
@@ -595,7 +595,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                             info.ExportConfig,
                             job);
 
-                    if (result.result == SaveResult.Success)
+                    if (result.result.ResultType == SaveResultType.Success)
                     {
                         Dispatcher.UIThread.Post(() =>
                         {
@@ -609,7 +609,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                     {
                         Dispatcher.UIThread.Post(() =>
                         {
-                            ShowSaveError((DialogSaveResult)result.result);
+                            ShowSaveError(result.result);
                         });
                     }
                 });
@@ -623,23 +623,28 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    private void ShowSaveError(DialogSaveResult result)
+    private void ShowSaveError(SaveResult result)
     {
-        switch (result)
+        switch (result.ResultType)
         {
-            case DialogSaveResult.InvalidPath:
+            case SaveResultType.InvalidPath:
                 NoticeDialog.Show("ERROR_SAVE_LOCATION", "ERROR");
                 break;
-            case DialogSaveResult.ConcurrencyError:
+            case SaveResultType.ConcurrencyError:
                 NoticeDialog.Show("INTERNAL_ERROR", "ERROR_WHILE_SAVING");
                 break;
-            case DialogSaveResult.SecurityError:
+            case SaveResultType.SecurityError:
                 NoticeDialog.Show(title: "SECURITY_ERROR", message: "SECURITY_ERROR_MSG");
                 break;
-            case DialogSaveResult.IoError:
+            case SaveResultType.IoError:
                 NoticeDialog.Show(title: "IO_ERROR", message: "IO_ERROR_MSG");
                 break;
-            case DialogSaveResult.UnknownError:
+            case SaveResultType.CustomError:
+                NoticeDialog.Show(result.ErrorMessage, "ERROR");
+                break;
+            case SaveResultType.Cancelled:
+                break;
+            case SaveResultType.UnknownError:
                 NoticeDialog.Show("UNKNOWN_ERROR_SAVING", "ERROR");
                 break;
         }

+ 3 - 0
src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs

@@ -46,7 +46,10 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
             activeWindow = value;
             OnPropertyChanged(nameof(ActiveWindow));
             if (activeWindow is ViewportWindowViewModel viewport)
+            {
+                Owner.LayoutSubViewModel.LayoutManager.ShowViewport(viewport);
                 ActiveViewportChanged?.Invoke(this, viewport);
+            }
         }
     }
 

+ 7 - 7
src/PixiEditor/ViewModels/UserPreferences/Settings/GeneralSettings.cs

@@ -8,19 +8,12 @@ namespace PixiEditor.ViewModels.UserPreferences.Settings;
 
 internal class GeneralSettings : SettingsGroup
 {
-    private bool imagePreviewInTaskbar = GetPreference(nameof(ImagePreviewInTaskbar), false);
     private LanguageData? selectedLanguage = ILocalizationProvider.Current?.SelectedLanguage;
     private List<LanguageData>? availableLanguages = ILocalizationProvider.Current?.LocalizationData.Languages
         .OrderByDescending(x => x == ILocalizationProvider.Current.FollowSystem)
         .ThenByDescending(x => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code || CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code)
         .ThenBy(x => x.Name).ToList();
 
-    public bool ImagePreviewInTaskbar
-    {
-        get => imagePreviewInTaskbar;
-        set => RaiseAndUpdatePreference(ref imagePreviewInTaskbar, value);
-    }
-
     private bool isDebugModeEnabled = GetPreference(nameof(IsDebugModeEnabled), false);
     public bool IsDebugModeEnabled
     {
@@ -46,4 +39,11 @@ internal class GeneralSettings : SettingsGroup
             }
         }
     }
+
+    private bool isAnalyticsEnabled = GetPreference(PreferencesConstants.AnalyticsEnabled, PreferencesConstants.AnalyticsEnabledDefault);
+    public bool AnalyticsEnabled
+    {
+        get => isAnalyticsEnabled;
+        set => RaiseAndUpdatePreference(ref isAnalyticsEnabled, value);
+    }
 }

+ 0 - 1
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -307,7 +307,6 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         const string ConfirmationDialogTitle = "UNSAVED_CHANGES";
         const string ConfirmationDialogMessage = "DOCUMENT_MODIFIED_SAVE";
 
-
         ConfirmationType result = ConfirmationType.No;
         bool saved = false;
         if (!document.AllChangesSaved)

+ 3 - 0
src/PixiEditor/Views/Rendering/Scene.cs

@@ -200,6 +200,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     protected override async void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
     {
+        framebuffer?.Dispose();
+        framebuffer = null;
+
         if (initialized)
         {
             surface.Dispose();

+ 14 - 3
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -57,6 +57,11 @@
                         <Style Selector=":is(Control).leftOffset">
                             <Setter Property="Margin" Value="20, 0, 0, 0" />
                         </Style>
+                        <Style Selector=":is(TextBlock).subtext">
+                            <Setter Property="FontSize" Value="12" />
+                            <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLowBrush}" />
+                            <Setter Property="TextWrapping" Value="Wrap" />
+                        </Style>
                     </Grid.Styles>
                     <ScrollViewer>
                         <ScrollViewer.IsVisible>
@@ -88,6 +93,8 @@
                                 </ComboBox.ItemTemplate>
                             </ComboBox>
 
+                            <TextBlock Classes="leftOffset subtext" ui:Translator.Key="LANGUAGE_INFO" />
+
                             <TextBlock ui:Translator.Key="MISC" Classes="h5" />
 
                             <CheckBox Classes="leftOffset" ui:Translator.Key="SHOW_STARTUP_WINDOW"
@@ -96,9 +103,6 @@
                             <CheckBox Classes="leftOffset" ui:Translator.Key="DISABLE_NEWS_PANEL"
                                       IsChecked="{Binding SettingsSubViewModel.File.DisableNewsPanel}" />
 
-                            <CheckBox Classes="leftOffset" ui:Translator.Key="SHOW_IMAGE_PREVIEW_TASKBAR"
-                                      IsChecked="{Binding SettingsSubViewModel.General.ImagePreviewInTaskbar}" />
-
                             <StackPanel Classes="leftOffset" Orientation="Horizontal">
                                 <Label
                                     ui:Translator.Key="RECENT_FILE_LENGTH"
@@ -208,6 +212,13 @@
                             <CheckBox Classes="leftOffset"
                                       IsChecked="{Binding SettingsSubViewModel.General.IsDebugModeEnabled}"
                                       ui:Translator.Key="ENABLE_DEBUG_MODE" d:Content="Enable Debug Mode" />
+
+                            <TextBlock ui:Translator.Key="MISC"/>
+
+                            <CheckBox Classes="leftOffset"
+                                      IsChecked="{Binding SettingsSubViewModel.General.AnalyticsEnabled, Mode=TwoWay}"
+                                      ui:Translator.Key="ENABLE_ANALYTICS" d:Content="Enable Analytics" />
+                            <TextBlock ui:Translator.Key="ANALYTICS_INFO" Classes="leftOffset subtext"/>
                             <!--<Label Classes="{StaticResource SettingsText}" VerticalAlignment="Center">
                             <ui1:Hyperlink Command="{cmds:Command PixiEditor.Debug.OpenCrashReportsDirectory}" Style="{StaticResource SettingsLink}">
                                 <Run ui:Translator.Key="OPEN_CRASH_REPORTS_DIR" d:Text="Open crash reports directory"/>