Browse Source

Svg export settings with image filtering

flabbet 11 months ago
parent
commit
12436fca8e

+ 3 - 1
src/PixiEditor.SVG/Elements/SvgImage.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Units;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
 
@@ -12,6 +13,7 @@ public class SvgImage : SvgElement
         
     public SvgProperty<SvgStringUnit> Href { get; } = new("xlink:href");
     public SvgProperty<SvgLinkUnit> Mask { get; } = new("mask");
+    public SvgProperty<SvgEnumUnit<SvgImageRenderingType>> ImageRendering { get; } = new("image-rendering");
 
     public SvgImage() : base("image")
     {

+ 10 - 0
src/PixiEditor.SVG/Enums/SvgImageRenderingType.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgImageRenderingType
+{
+    Auto = 0,
+    Smooth = 1,
+    HighQuality = 2,
+    CrispEdges = 3,
+    Pixelated = 4
+}

+ 9 - 0
src/PixiEditor.SVG/Helpers/StringExtensions.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.SVG.Helpers;
+
+public static class StringExtensions
+{
+    public static string ToKebabCase(this string str)
+    {
+        return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLower();
+    }
+}

+ 18 - 0
src/PixiEditor.SVG/Units/SvgEnumUnit.cs

@@ -0,0 +1,18 @@
+using PixiEditor.SVG.Helpers;
+
+namespace PixiEditor.SVG.Units;
+
+public struct SvgEnumUnit<T> : ISvgUnit where T : Enum
+{
+    public T Value { get; set; }
+
+    public SvgEnumUnit(T value)
+    {
+        Value = value;
+    }
+
+    public string ToXml()
+    {
+        return Value.ToString().ToKebabCase();
+    }
+}

+ 5 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs

@@ -17,6 +17,8 @@ internal class VectorEllipseToolExecutor : ShapeToolExecutor<IVectorEllipseToolH
 
     private VecD firstRadius;
     private VecD firstCenter;
+    
+    private Matrix3X3 lastMatrix = Matrix3X3.Identity;
 
     protected override void DrawShape(VecI curPos, double rotationRad, bool firstDraw)
     {
@@ -46,7 +48,8 @@ internal class VectorEllipseToolExecutor : ShapeToolExecutor<IVectorEllipseToolH
         return new SetShapeGeometry_Action(memberGuid,
             new EllipseVectorData(firstCenter, firstRadius)
             {
-                StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth
+                StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth,
+                TransformationMatrix = lastMatrix
             });
     }
 
@@ -66,6 +69,7 @@ internal class VectorEllipseToolExecutor : ShapeToolExecutor<IVectorEllipseToolH
         };
 
         lastRect = rect;
+        lastMatrix = matrix;
 
         return new SetShapeGeometry_Action(memberGuid, ellipseData);
     }

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

@@ -19,7 +19,7 @@ internal class SvgFileType : IoFileType
     public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job)
     {
         job?.Report(0, string.Empty);
-        SvgDocument svgDocument = document.ToSvgDocument(0, config.ExportSize);
+        SvgDocument svgDocument = document.ToSvgDocument(0, config.ExportSize, config.VectorExportConfig);
 
         job?.Report(0.5, string.Empty); 
         string xml = svgDocument.ToXml();

+ 2 - 0
src/PixiEditor/Models/IO/ExportConfig.cs

@@ -12,4 +12,6 @@ public class ExportConfig
    public int SpriteSheetColumns { get; set; }
    public int SpriteSheetRows { get; set; }
    public IAnimationRenderer? AnimationRenderer { get; set; }
+   
+   public VectorExportConfig? VectorExportConfig { get; set; }
 }

+ 6 - 0
src/PixiEditor/Models/IO/VectorExportConfig.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.IO;
+
+public class VectorExportConfig
+{
+    public bool UseNearestNeighborForImageUpscaling { get; set; } = false;
+}

+ 24 - 15
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -20,6 +20,7 @@ using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Handlers;
+using PixiEditor.Models.IO;
 using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Numerics;
@@ -30,6 +31,7 @@ using PixiEditor.Parser.Skia;
 using PixiEditor.Parser.Skia.Encoders;
 using PixiEditor.SVG;
 using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.ViewModels.Document.Nodes;
@@ -77,7 +79,7 @@ internal partial class DocumentViewModel
         return document;
     }
 
-    public SvgDocument ToSvgDocument(KeyFrameTime atTime, VecI exportSize)
+    public SvgDocument ToSvgDocument(KeyFrameTime atTime, VecI exportSize, VectorExportConfig? vectorExportConfig)
     {
         SvgDocument svgDocument = new(new RectD(0, 0, exportSize.X, exportSize.Y));
 
@@ -85,13 +87,13 @@ internal partial class DocumentViewModel
         float resizeFactorY = (float)exportSize.Y / Height;
         VecD resizeFactor = new VecD(resizeFactorX, resizeFactorY);
 
-        AddElements(NodeGraph.StructureTree.Members, svgDocument, atTime, resizeFactor);
+        AddElements(NodeGraph.StructureTree.Members, svgDocument, atTime, resizeFactor, vectorExportConfig);
 
         return svgDocument;
     }
 
     private void AddElements(IEnumerable<INodeHandler> root, IElementContainer elementContainer, KeyFrameTime atTime,
-        VecD resizeFactor)
+        VecD resizeFactor, VectorExportConfig? vectorExportConfig)
     {
         foreach (var member in root)
         {
@@ -99,32 +101,33 @@ internal partial class DocumentViewModel
             {
                 var group = new SvgGroup();
 
-                AddElements(folderNodeViewModel.Children, group, atTime, resizeFactor);
+                AddElements(folderNodeViewModel.Children, group, atTime, resizeFactor, vectorExportConfig);
                 elementContainer.Children.Add(group);
             }
 
             if (member is IRasterLayerHandler)
             {
-                AddSvgImage(elementContainer, atTime, member, resizeFactor);
+                AddSvgImage(elementContainer, atTime, member, resizeFactor, 
+                    vectorExportConfig?.UseNearestNeighborForImageUpscaling ?? false);
             }
             else if (member is IVectorLayerHandler vectorLayerHandler)
             {
-                AddSvgShape(elementContainer, atTime, vectorLayerHandler, resizeFactor);
+                AddSvgShape(elementContainer, vectorLayerHandler, resizeFactor);
             }
         }
     }
 
-    private void AddSvgShape(IElementContainer elementContainer, KeyFrameTime atTime, IVectorLayerHandler vectorLayerHandler, VecD resizeFactor)
+    private void AddSvgShape(IElementContainer elementContainer, IVectorLayerHandler vectorLayerHandler, VecD resizeFactor)
     {
         IReadOnlyVectorNode vectorNode = (IReadOnlyVectorNode)Internals.Tracker.Document.FindNode(vectorLayerHandler.Id);
 
         if (vectorNode.ShapeData is IReadOnlyEllipseData ellipseData)
         {
             SvgEllipse ellipse = new SvgEllipse();
-            ellipse.Cx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.X);
-            ellipse.Cy.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.Y);
-            ellipse.Rx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.X);
-            ellipse.Ry.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.Y);
+            ellipse.Cx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.X * resizeFactor.X);
+            ellipse.Cy.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Center.Y * resizeFactor.Y);
+            ellipse.Rx.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.X * resizeFactor.X);
+            ellipse.Ry.Unit = SvgNumericUnit.FromUserUnits(ellipseData.Radius.Y * resizeFactor.Y);
             ellipse.Fill.Unit = SvgColorUnit.FromRgba(ellipseData.FillColor.R, ellipseData.FillColor.G, ellipseData.FillColor.B, ellipseData.FillColor.A);
             ellipse.Stroke.Unit = SvgColorUnit.FromRgba(ellipseData.StrokeColor.R, ellipseData.StrokeColor.G, ellipseData.StrokeColor.B, ellipseData.StrokeColor.A);
             ellipse.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(ellipseData.StrokeWidth);
@@ -135,7 +138,7 @@ internal partial class DocumentViewModel
     }
 
     private void AddSvgImage(IElementContainer elementContainer, KeyFrameTime atTime, INodeHandler member,
-        VecD resizeFactor)
+        VecD resizeFactor, bool useNearestNeighborForImageUpscaling) 
     {
         IReadOnlyImageNode imageNode = (IReadOnlyImageNode)Internals.Tracker.Document.FindNode(member.Id);
 
@@ -153,14 +156,14 @@ internal partial class DocumentViewModel
 
             toSave = surface.DrawingSurface.Snapshot((RectI)tightBounds.Value);
         });
-
-        var image = CreateImageElement(resizeFactor, tightBounds.Value, toSave);
+        
+        var image = CreateImageElement(resizeFactor, tightBounds.Value, toSave, useNearestNeighborForImageUpscaling);
 
         elementContainer.Children.Add(image);
     }
 
     private static SvgImage CreateImageElement(VecD resizeFactor, RectD tightBounds,
-        Image toSerialize)
+        Image toSerialize, bool useNearestNeighborForImageUpscaling)
     {
         SvgImage image = new SvgImage();
 
@@ -195,6 +198,12 @@ internal partial class DocumentViewModel
         image.Width.Unit = SvgNumericUnit.FromUserUnits(targetBounds.Width);
         image.Height.Unit = SvgNumericUnit.FromUserUnits(targetBounds.Height);
         image.Href.Unit = new SvgStringUnit($"data:image/png;base64,{Convert.ToBase64String(targetBytes)}");
+        
+        if (useNearestNeighborForImageUpscaling)
+        {
+            image.ImageRendering.Unit = new SvgEnumUnit<SvgImageRenderingType>(SvgImageRenderingType.Pixelated);
+        }
+        
         return image;
     }
 

+ 9 - 0
src/PixiEditor/Views/Dialogs/ExportFileDialog.cs

@@ -116,6 +116,15 @@ internal class ExportFileDialog : CustomDialog
             ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;
             ExportConfig.SpriteSheetColumns = popup.SpriteSheetColumns;
             ExportConfig.SpriteSheetRows = popup.SpriteSheetRows;
+            
+            if (ChosenFormat is SvgFileType)
+            {
+                ExportConfig.VectorExportConfig = new VectorExportConfig()
+                {
+                    UseNearestNeighborForImageUpscaling =
+                        ExportConfig.ExportSize.X < 512 || ExportConfig.ExportSize.Y < 512
+                };
+            }
         }
 
         return result;