Răsfoiți Sursa

Added opacity and blending

Krzysztof Krysiński 5 luni în urmă
părinte
comite
e63d99cb41

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

@@ -5,7 +5,7 @@ using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 namespace PixiEditor.SVG.Elements;
 
 
-public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IElementContainer
+public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IOpacity, IElementContainer
 {
 {
     public List<SvgElement> Children { get; } = new();
     public List<SvgElement> Children { get; } = new();
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
@@ -14,10 +14,12 @@ public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
 
 
     public override void ParseData(XmlReader reader)
     public override void ParseData(XmlReader reader)
     {
     {
         List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth, StrokeLineCap, StrokeLineJoin };
         List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth, StrokeLineCap, StrokeLineJoin };
         ParseAttributes(properties, reader);
         ParseAttributes(properties, reader);
     }
     }
+
 }
 }

+ 4 - 1
src/PixiEditor.SVG/Elements/SvgPrimitive.cs

@@ -5,7 +5,7 @@ using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 namespace PixiEditor.SVG.Elements;
 
 
-public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable
+public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable, IOpacity
 {
 {
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
@@ -16,6 +16,8 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
     
     
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
 
 
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
+
     public override void ParseData(XmlReader reader)
     public override void ParseData(XmlReader reader)
     {
     {
         List<SvgProperty> properties = GetProperties().ToList();
         List<SvgProperty> properties = GetProperties().ToList();
@@ -26,6 +28,7 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
         properties.Add(StrokeWidth);
         properties.Add(StrokeWidth);
         properties.Add(StrokeLineCap);
         properties.Add(StrokeLineCap);
         properties.Add(StrokeLineJoin);
         properties.Add(StrokeLineJoin);
+        properties.Add(Opacity);
 
 
         do
         do
         {
         {

+ 8 - 0
src/PixiEditor.SVG/Features/IOpacity.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface IOpacity
+{
+    public SvgProperty<SvgNumericUnit> Opacity { get; }
+}

+ 23 - 2
src/PixiEditor.SVG/StyleContext.cs

@@ -10,10 +10,9 @@ public struct StyleContext
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
-    
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
-    
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
+    public SvgProperty<SvgNumericUnit> Opacity { get; }
 
 
     public StyleContext()
     public StyleContext()
     {
     {
@@ -24,6 +23,7 @@ public struct StyleContext
         Transform = new("transform");
         Transform = new("transform");
         StrokeLineCap = new("stroke-linecap");
         StrokeLineCap = new("stroke-linecap");
         StrokeLineJoin = new("stroke-linejoin");
         StrokeLineJoin = new("stroke-linejoin");
+        Opacity = new("opacity");
     }
     }
     
     
     public StyleContext(SvgDocument document)
     public StyleContext(SvgDocument document)
@@ -34,6 +34,7 @@ public struct StyleContext
         Transform = document.Transform;
         Transform = document.Transform;
         StrokeLineCap = document.StrokeLineCap;
         StrokeLineCap = document.StrokeLineCap;
         StrokeLineJoin = document.StrokeLineJoin;
         StrokeLineJoin = document.StrokeLineJoin;
+        Opacity = document.Opacity;
     }
     }
 
 
     public StyleContext WithElement(SvgElement element)
     public StyleContext WithElement(SvgElement element)
@@ -73,6 +74,11 @@ public struct StyleContext
             }
             }
         }
         }
 
 
+        if(element is IOpacity opacityElement)
+        {
+            styleContext.Opacity.Unit = opacityElement.Opacity.Unit;
+        }
+
         return styleContext;
         return styleContext;
     }
     }
 
 
@@ -99,6 +105,21 @@ public struct StyleContext
             styleContext.Transform.Unit = Transform.Unit;
             styleContext.Transform.Unit = Transform.Unit;
         }
         }
 
 
+        if (StrokeLineCap.Unit != null)
+        {
+            styleContext.StrokeLineCap.Unit = StrokeLineCap.Unit;
+        }
+
+        if (StrokeLineJoin.Unit != null)
+        {
+            styleContext.StrokeLineJoin.Unit = StrokeLineJoin.Unit;
+        }
+
+        if (Opacity.Unit != null)
+        {
+            styleContext.Opacity.Unit = Opacity.Unit;
+        }
+
         return styleContext;
         return styleContext;
     }
     }
 }
 }

+ 4 - 2
src/PixiEditor.SVG/SvgDocument.cs

@@ -7,7 +7,7 @@ using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG;
 namespace PixiEditor.SVG;
 
 
-public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable
+public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable, IOpacity
 {
 {
     public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string Version { get; set; } = "1.1";
     public string Version { get; set; } = "1.1";
@@ -21,6 +21,7 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
     
     
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
     public List<SvgElement> Children { get; } = new();
     public List<SvgElement> Children { get; } = new();
 
 
     public SvgDocument() : base("svg")
     public SvgDocument() : base("svg")
@@ -43,7 +44,8 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
             Transform,
             Transform,
             ViewBox,
             ViewBox,
             StrokeLineCap,
             StrokeLineCap,
-            StrokeLineJoin
+            StrokeLineJoin,
+            Opacity
         };
         };
         
         
         ParseAttributes(properties, reader);
         ParseAttributes(properties, reader);

+ 80 - 4
src/PixiEditor.SVG/Units/SvgTransformUnit.cs

@@ -11,12 +11,12 @@ public struct SvgTransformUnit : ISvgUnit
     }
     }
 
 
     public Matrix3X3 MatrixValue { get; set; } = Matrix3X3.Identity;
     public Matrix3X3 MatrixValue { get; set; } = Matrix3X3.Identity;
-    
+
     public SvgTransformUnit(Matrix3X3 matrixValue)
     public SvgTransformUnit(Matrix3X3 matrixValue)
     {
     {
         MatrixValue = matrixValue;
         MatrixValue = matrixValue;
     }
     }
-    
+
     public string ToXml()
     public string ToXml()
     {
     {
         string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
         string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
@@ -25,7 +25,7 @@ public struct SvgTransformUnit : ISvgUnit
         string scaleY = MatrixValue.ScaleY.ToString(CultureInfo.InvariantCulture);
         string scaleY = MatrixValue.ScaleY.ToString(CultureInfo.InvariantCulture);
         string skewX = MatrixValue.SkewX.ToString(CultureInfo.InvariantCulture);
         string skewX = MatrixValue.SkewX.ToString(CultureInfo.InvariantCulture);
         string skewY = MatrixValue.SkewY.ToString(CultureInfo.InvariantCulture);
         string skewY = MatrixValue.SkewY.ToString(CultureInfo.InvariantCulture);
-        
+
         return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
         return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
     }
     }
 
 
@@ -49,7 +49,83 @@ public struct SvgTransformUnit : ISvgUnit
         }
         }
         else
         else
         {
         {
-            // todo: parse other types of transformation syntax (rotate, translate, scale etc)
+            MatrixValue = TryParseDescriptiveTransform(readerValue);
+        }
+    }
+
+    private Matrix3X3 TryParseDescriptiveTransform(string readerValue)
+    {
+        if (!readerValue.Contains('(') || !readerValue.Contains(')'))
+        {
+            return Matrix3X3.Identity;
+        }
+
+        string[] parts = readerValue.Split('(');
+        string transformType = parts[0];
+        string[] values = parts[1].Split(' ', ')').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+
+        if (values.Length == 0)
+        {
+            return Matrix3X3.Identity;
+        }
+
+        if (transformType == "translate")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float translateX))
+            {
+                float translateY = translateX;
+                if (values.Length > 1)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out translateY);
+                }
+
+                return Matrix3X3.CreateTranslation(translateX, translateY);
+            }
+        }
+        else if (transformType == "scale")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float scaleX))
+            {
+                float scaleY = scaleX;
+                if (values.Length > 1)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out scaleY);
+                }
+
+                return Matrix3X3.CreateScale(scaleX, scaleY);
+            }
+        }
+        else if (transformType == "rotate")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float angle))
+            {
+                float radians = angle * (float)Math.PI / 180;
+
+                if(values.Length > 2)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float centerX);
+                    float.TryParse(values[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float centerY);
+                    return Matrix3X3.CreateRotation(radians, centerX, centerY);
+                }
+
+                return Matrix3X3.CreateRotation(radians);
+            }
+        }
+        else if (transformType == "skewX")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewX))
+            {
+                return Matrix3X3.CreateSkew(skewX, 0);
+            }
         }
         }
+        else if (transformType == "skewY")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewY))
+            {
+                return Matrix3X3.CreateSkew(0, skewY);
+            }
+        }
+
+        return Matrix3X3.Identity;
     }
     }
 }
 }

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

@@ -799,7 +799,7 @@
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "FILL_MODE": "Fill mode",
   "FILL_MODE": "Fill mode",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
-  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using legacy blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
+  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using sRGB blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
   "FILL_TYPE_WINDING": "Winding",
   "FILL_TYPE_WINDING": "Winding",
   "FILL_TYPE_EVEN_ODD": "Even Odd",
   "FILL_TYPE_EVEN_ODD": "Even Odd",
   "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
   "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
@@ -849,5 +849,7 @@
   "BOLD_TOOLTIP": "Bold",
   "BOLD_TOOLTIP": "Bold",
   "ITALIC_TOOLTIP": "Italic",
   "ITALIC_TOOLTIP": "Italic",
   "CUSTOM_FONT": "Custom font",
   "CUSTOM_FONT": "Custom font",
-  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics"
+  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics",
+  "USE_SRGB_PROCESSING": "Use sRGB for processing colors",
+  "USE_SRGB_PROCESSING_DESC": "Convert document using linear sRGB to sRGB for processing colors. This will affect the colors of the document."
 }
 }

+ 3 - 3
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -33,7 +33,7 @@ internal class DocumentViewModelBuilder
 
 
     public NodeGraphBuilder Graph { get; set; }
     public NodeGraphBuilder Graph { get; set; }
     public string ImageEncoderUsed { get; set; } = "QOI";
     public string ImageEncoderUsed { get; set; } = "QOI";
-    public bool UsesLegacyColorBlending { get; set; } = false;
+    public bool UsesSrgbColorBlending { get; set; } = false;
     public Version? PixiParserVersionUsed { get; set; }
     public Version? PixiParserVersionUsed { get; set; }
     public ResourceStorage DocumentResources { get; set; }
     public ResourceStorage DocumentResources { get; set; }
 
 
@@ -127,9 +127,9 @@ internal class DocumentViewModelBuilder
         return this;
         return this;
     }
     }
 
 
-    public DocumentViewModelBuilder WithLegacyColorBlending(bool usesLegacyColorBlending)
+    public DocumentViewModelBuilder? WithSrgbColorBlending(bool usesLegacyColorBlending)
     {
     {
-        UsesLegacyColorBlending = usesLegacyColorBlending;
+        UsesSrgbColorBlending = usesLegacyColorBlending;
         return this;
         return this;
     }
     }
 
 

+ 1 - 1
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -32,7 +32,7 @@ internal static class PixiParserDocumentEx
         return DocumentViewModel.Build(b => b
         return DocumentViewModel.Build(b => b
             .WithPixiParserVersion(document.Version)
             .WithPixiParserVersion(document.Version)
             .WithSerializerData(document.SerializerName, document.SerializerVersion)
             .WithSerializerData(document.SerializerName, document.SerializerVersion)
-            .WithLegacyColorBlending(document.LegacyColorBlending)
+            .WithSrgbColorBlending(document.SrgbColorBlending)
             .WithSize(document.Width, document.Height)
             .WithSize(document.Width, document.Height)
             .WithImageEncoder(document.ImageEncoderUsed)
             .WithImageEncoder(document.ImageEncoderUsed)
             .WithPalette(document.Palette, color => new PaletteColor(color.R, color.G, color.B))
             .WithPalette(document.Palette, color => new PaletteColor(color.R, color.G, color.B))

+ 2 - 0
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -34,6 +34,7 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         }
         }
 
 
         builder.WithSize(size)
         builder.WithSize(size)
+            .WithSrgbColorBlending(true) // apparently svgs blend colors in SRGB space
             .WithGraph(graph =>
             .WithGraph(graph =>
             {
             {
                 int? lastId = null;
                 int? lastId = null;
@@ -91,6 +92,7 @@ internal class SvgDocumentBuilder : IDocumentBuilder
 
 
         NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<VectorLayerNode>(out int id)
         NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<VectorLayerNode>(out int id)
             .WithName(name)
             .WithName(name)
+            .WithInputValues(new Dictionary<string, object>() { { StructureNode.OpacityPropertyName, (float)(styleContext.Opacity.Unit?.Value ?? 1f) } })
             .WithAdditionalData(new Dictionary<string, object>() { { "ShapeData", shapeData } });
             .WithAdditionalData(new Dictionary<string, object>() { { "ShapeData", shapeData } });
 
 
         if (lastId != null)
         if (lastId != null)

+ 85 - 40
src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs

@@ -21,6 +21,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
     public event EventHandler<DocumentChangedEventArgs>? ActiveDocumentChanged;
     public event EventHandler<DocumentChangedEventArgs>? ActiveDocumentChanged;
 
 
     private DocumentViewModel? activeDocument;
     private DocumentViewModel? activeDocument;
+
     public DocumentViewModel? ActiveDocument
     public DocumentViewModel? ActiveDocument
     {
     {
         get => activeDocument;
         get => activeDocument;
@@ -33,11 +34,12 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
             activeDocument = value;
             activeDocument = value;
             OnPropertyChanged(nameof(ActiveDocument));
             OnPropertyChanged(nameof(ActiveDocument));
             ActiveDocumentChanged?.Invoke(this, new(value, prevDoc));
             ActiveDocumentChanged?.Invoke(this, new(value, prevDoc));
-            
+
             if (ViewModelMain.Current.ToolsSubViewModel.ActiveTool == null)
             if (ViewModelMain.Current.ToolsSubViewModel.ActiveTool == null)
             {
             {
                 var firstTool =
                 var firstTool =
-                    ViewModelMain.Current.ToolsSubViewModel.ActiveToolSet.Tools.FirstOrDefault(x => x.CanBeUsedOnActiveLayer);
+                    ViewModelMain.Current.ToolsSubViewModel.ActiveToolSet.Tools.FirstOrDefault(x =>
+                        x.CanBeUsedOnActiveLayer);
                 if (firstTool != null)
                 if (firstTool != null)
                 {
                 {
                     ViewModelMain.Current.ToolsSubViewModel.SetActiveTool(firstTool.GetType(), false);
                     ViewModelMain.Current.ToolsSubViewModel.SetActiveTool(firstTool.GetType(), false);
@@ -67,57 +69,76 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
     [Evaluator.CanExecute("PixiEditor.HasDocument", nameof(ActiveDocument))]
     [Evaluator.CanExecute("PixiEditor.HasDocument", nameof(ActiveDocument))]
     public bool DocumentNotNull() => ActiveDocument != null;
     public bool DocumentNotNull() => ActiveDocument != null;
 
 
-    [Command.Basic("PixiEditor.Document.ClipCanvas", "CLIP_CANVAS", "CLIP_CANVAS", CanExecute = "PixiEditor.HasDocument",
+    [Command.Basic("PixiEditor.Document.ClipCanvas", "CLIP_CANVAS", "CLIP_CANVAS",
+        CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.Crop, MenuItemPath = "IMAGE/CLIP_CANVAS", MenuItemOrder = 2, AnalyticsTrack = true)]
         Icon = PixiPerfectIcons.Crop, MenuItemPath = "IMAGE/CLIP_CANVAS", MenuItemOrder = 2, AnalyticsTrack = true)]
     public void ClipCanvas() => ActiveDocument?.Operations.ClipCanvas();
     public void ClipCanvas() => ActiveDocument?.Operations.ClipCanvas();
 
 
-    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "FLIP_IMG_HORIZONTALLY", "FLIP_IMG_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_HORIZONTALLY", MenuItemOrder = 14, Icon = PixiPerfectIcons.XFlip, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "FLIP_IMG_VERTICALLY", "FLIP_IMG_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_VERTICALLY", MenuItemOrder = 15, Icon = PixiPerfectIcons.YFlip, AnalyticsTrack = true)]
-    public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type, activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "FLIP_IMG_HORIZONTALLY",
+        "FLIP_IMG_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_HORIZONTALLY", MenuItemOrder = 14, Icon = PixiPerfectIcons.XFlip,
+        AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "FLIP_IMG_VERTICALLY",
+        "FLIP_IMG_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_VERTICALLY", MenuItemOrder = 15, Icon = PixiPerfectIcons.YFlip,
+        AnalyticsTrack = true)]
+    public void FlipImage(FlipType type) =>
+        ActiveDocument?.Operations.FlipImage(type, activeDocument.AnimationDataViewModel.ActiveFrameBindable);
 
 
-    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY", "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16, Icon = PixiPerfectIcons.XSelectedFlip, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "FLIP_LAYERS_VERTICALLY", "FLIP_LAYERS_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_VERTICALLY", MenuItemOrder = 17, Icon = PixiPerfectIcons.YSelectedFlip, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY",
+        "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16, Icon = PixiPerfectIcons.XSelectedFlip,
+        AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "FLIP_LAYERS_VERTICALLY",
+        "FLIP_LAYERS_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_VERTICALLY", MenuItemOrder = 17, Icon = PixiPerfectIcons.YSelectedFlip,
+        AnalyticsTrack = true)]
     public void FlipLayers(FlipType type)
     public void FlipLayers(FlipType type)
     {
     {
         if (ActiveDocument?.SelectedStructureMember == null)
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
             return;
 
 
-        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers(),
+            activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
     }
 
 
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "ROT_IMG_90",
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "ROT_IMG_90",
         "ROT_IMG_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
         "ROT_IMG_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
-        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_90_D", MenuItemOrder = 8, Icon = PixiPerfectIcons.RotateImage90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_90_D", MenuItemOrder = 8, Icon = PixiPerfectIcons.RotateImage90,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate180Deg", "ROT_IMG_180",
     [Command.Basic("PixiEditor.Document.Rotate180Deg", "ROT_IMG_180",
         "ROT_IMG_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
         "ROT_IMG_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
-        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_180_D", MenuItemOrder = 9, Icon = PixiPerfectIcons.RotateImage180, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_180_D", MenuItemOrder = 9, Icon = PixiPerfectIcons.RotateImage180,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "ROT_IMG_-90",
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "ROT_IMG_-90",
         "ROT_IMG_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
         "ROT_IMG_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
-        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_-90_D", MenuItemOrder = 10, Icon = PixiPerfectIcons.RotateImageMinus90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_-90_D", MenuItemOrder = 10, Icon = PixiPerfectIcons.RotateImageMinus90,
+        AnalyticsTrack = true)]
     public void RotateImage(RotationAngle angle) => ActiveDocument?.Operations.RotateImage(angle);
     public void RotateImage(RotationAngle angle) => ActiveDocument?.Operations.RotateImage(angle);
 
 
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "ROT_LAYERS_90",
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "ROT_LAYERS_90",
         "ROT_LAYERS_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
         "ROT_LAYERS_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_90_D", MenuItemOrder = 11, Icon = PixiPerfectIcons.RotateFile90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_90_D", MenuItemOrder = 11, Icon = PixiPerfectIcons.RotateFile90,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "ROT_LAYERS_180",
     [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "ROT_LAYERS_180",
         "ROT_LAYERS_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
         "ROT_LAYERS_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_180_D", MenuItemOrder = 12, Icon = PixiPerfectIcons.RotateFile180, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_180_D", MenuItemOrder = 12, Icon = PixiPerfectIcons.RotateFile180,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "ROT_LAYERS_-90",
     [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "ROT_LAYERS_-90",
         "ROT_LAYERS_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
         "ROT_LAYERS_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_-90_D", MenuItemOrder = 13, Icon = PixiPerfectIcons.RotateFileMinus90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_-90_D", MenuItemOrder = 13, Icon = PixiPerfectIcons.RotateFileMinus90,
+        AnalyticsTrack = true)]
     public void RotateLayers(RotationAngle angle)
     public void RotateLayers(RotationAngle angle)
     {
     {
         if (ActiveDocument?.SelectedStructureMember == null)
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
             return;
-        
-        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+
+        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers(),
+            activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
     }
 
 
-    [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "TOGGLE_VERT_SYMMETRY_AXIS", "TOGGLE_VERT_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", 
+    [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "TOGGLE_VERT_SYMMETRY_AXIS",
+        "TOGGLE_VERT_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.YSymmetry, AnalyticsTrack = true)]
         Icon = PixiPerfectIcons.YSymmetry, AnalyticsTrack = true)]
     public void ToggleVerticalSymmetryAxis()
     public void ToggleVerticalSymmetryAxis()
     {
     {
@@ -126,7 +147,8 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.VerticalSymmetryAxisEnabledBindable ^= true;
         ActiveDocument.VerticalSymmetryAxisEnabledBindable ^= true;
     }
     }
 
 
-    [Command.Basic("PixiEditor.Document.ToggleHorizontalSymmetryAxis", "TOGGLE_HOR_SYMMETRY_AXIS", "TOGGLE_HOR_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", 
+    [Command.Basic("PixiEditor.Document.ToggleHorizontalSymmetryAxis", "TOGGLE_HOR_SYMMETRY_AXIS",
+        "TOGGLE_HOR_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.XSymmetry, AnalyticsTrack = true)]
         Icon = PixiPerfectIcons.XSymmetry, AnalyticsTrack = true)]
     public void ToggleHorizontalSymmetryAxis()
     public void ToggleHorizontalSymmetryAxis()
     {
     {
@@ -143,7 +165,8 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.EventInlet.OnSymmetryDragged(info);
         ActiveDocument.EventInlet.OnSymmetryDragged(info);
     }
     }
 
 
-    [Command.Internal("PixiEditor.Document.StartDragSymmetry", CanExecute = "PixiEditor.HasDocument", AnalyticsTrack = true)]
+    [Command.Internal("PixiEditor.Document.StartDragSymmetry", CanExecute = "PixiEditor.HasDocument",
+        AnalyticsTrack = true)]
     public void StartDragSymmetry(SymmetryAxisDirection dir)
     public void StartDragSymmetry(SymmetryAxisDirection dir)
     {
     {
         if (ActiveDocument is null)
         if (ActiveDocument is null)
@@ -160,21 +183,25 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.EventInlet.OnSymmetryDragEnded(dir);
         ActiveDocument.EventInlet.OnSymmetryDragEnded(dir);
     }
     }
 
 
-    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", 
+    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE",
         CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
         CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
         ShortcutContexts = [typeof(ViewportWindowViewModel)],
         ShortcutContexts = [typeof(ViewportWindowViewModel)],
         Icon = PixiPerfectIcons.Eraser,
         Icon = PixiPerfectIcons.Eraser,
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6, AnalyticsTrack = true)]
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6, AnalyticsTrack = true)]
     public void DeletePixels()
     public void DeletePixels()
     {
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument
+            .AnimationDataViewModel.ActiveFrameBindable);
     }
     }
 
 
 
 
-    [Command.Basic("PixiEditor.Document.ResizeDocument", false, "RESIZE_DOCUMENT", "RESIZE_DOCUMENT", CanExecute = "PixiEditor.HasDocument", Key = Key.I, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
+    [Command.Basic("PixiEditor.Document.ResizeDocument", false, "RESIZE_DOCUMENT", "RESIZE_DOCUMENT",
+        CanExecute = "PixiEditor.HasDocument", Key = Key.I, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
         Icon = PixiPerfectIcons.Resize, MenuItemPath = "IMAGE/RESIZE_IMAGE", MenuItemOrder = 0, AnalyticsTrack = true)]
         Icon = PixiPerfectIcons.Resize, MenuItemPath = "IMAGE/RESIZE_IMAGE", MenuItemOrder = 0, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Document.ResizeCanvas", true, "RESIZE_CANVAS", "RESIZE_CANVAS", CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
-        Icon = PixiPerfectIcons.CanvasResize, MenuItemPath = "IMAGE/RESIZE_CANVAS", MenuItemOrder = 1, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.ResizeCanvas", true, "RESIZE_CANVAS", "RESIZE_CANVAS",
+        CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
+        Icon = PixiPerfectIcons.CanvasResize, MenuItemPath = "IMAGE/RESIZE_CANVAS", MenuItemOrder = 1,
+        AnalyticsTrack = true)]
     public async Task OpenResizePopup(bool canvas)
     public async Task OpenResizePopup(bool canvas)
     {
     {
         DocumentViewModel? doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         DocumentViewModel? doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -199,26 +226,44 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         }
         }
     }
     }
 
 
-    [Command.Basic("PixiEditor.Document.CenterContent", "CENTER_CONTENT", "CENTER_CONTENT", CanExecute = "PixiEditor.HasDocument",
-        Icon = PixiPerfectIcons.Center, MenuItemPath = "IMAGE/CENTER_CONTENT", MenuItemOrder = 3, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.CenterContent", "CENTER_CONTENT", "CENTER_CONTENT",
+        CanExecute = "PixiEditor.HasDocument",
+        Icon = PixiPerfectIcons.Center, MenuItemPath = "IMAGE/CENTER_CONTENT", MenuItemOrder = 3,
+        AnalyticsTrack = true)]
     public void CenterContent()
     public void CenterContent()
     {
     {
-        if(ActiveDocument?.SelectedStructureMember == null)
+        if (ActiveDocument?.SelectedStructureMember == null)
             return;
             return;
-        
-        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+
+        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers(),
+            activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
     }
-    
-    [Command.Basic("PixiEditor.Document.UseLinearSrgbProcessing", "USE_LINEAR_SRGB_PROCESSING", "USE_LINEAR_SRGB_PROCESSING_DESC", CanExecute = "PixiEditor.DocumentUsesLegacyBlending", 
+
+    [Command.Basic("PixiEditor.Document.UseLinearSrgbProcessing", "USE_LINEAR_SRGB_PROCESSING",
+        "USE_LINEAR_SRGB_PROCESSING_DESC", CanExecute = "PixiEditor.DocumentUsesSrgbBlending",
         AnalyticsTrack = true)]
         AnalyticsTrack = true)]
     public void UseLinearSrgbProcessing()
     public void UseLinearSrgbProcessing()
     {
     {
         if (ActiveDocument is null)
         if (ActiveDocument is null)
             return;
             return;
-        
+
         ActiveDocument.Operations.UseLinearSrgbProcessing();
         ActiveDocument.Operations.UseLinearSrgbProcessing();
     }
     }
-    
-    [Evaluator.CanExecute("PixiEditor.DocumentUsesLegacyBlending", nameof(ActiveDocument))]
-    public bool DocumentUsesLegacyBlending() => ActiveDocument?.UsesLegacyBlending ?? false;
+
+    [Command.Basic("PixiEditor.Document.UseSrgbProcessing", "USE_SRGB_PROCESSING",
+        "USE_SRGB_PROCESSING_DESC", CanExecute = "PixiEditor.DocumentUsesLinearBlending",
+        AnalyticsTrack = true)]
+    public void UseSrgbProcessing()
+    {
+        if (ActiveDocument is null)
+            return;
+
+        ActiveDocument.Operations.UseSrgbProcessing();
+    }
+
+    [Evaluator.CanExecute("PixiEditor.DocumentUsesSrgbBlending", nameof(ActiveDocument))]
+    public bool DocumentUsesSrgbBlending() => ActiveDocument?.UsesSrgbBlending ?? false;
+
+    [Evaluator.CanExecute("PixiEditor.DocumentUsesLinearBlending", nameof(ActiveDocument))]
+    public bool DocumentUsesLinearBlending() => !ActiveDocument?.UsesSrgbBlending ?? true;
 }
 }

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

@@ -75,7 +75,7 @@ internal partial class DocumentViewModel
         {
         {
             SerializerName = "PixiEditor",
             SerializerName = "PixiEditor",
             SerializerVersion = VersionHelpers.GetCurrentAssemblyVersion().ToString(),
             SerializerVersion = VersionHelpers.GetCurrentAssemblyVersion().ToString(),
-            LegacyColorBlending = doc.ProcessingColorSpace.IsSrgb,
+            SrgbColorBlending = doc.ProcessingColorSpace.IsSrgb,
             Width = Width,
             Width = Width,
             Height = Height,
             Height = Height,
             Swatches = ToCollection(Swatches),
             Swatches = ToCollection(Swatches),

+ 6 - 6
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -224,7 +224,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     ILineOverlayHandler IDocument.LineToolOverlayHandler => LineToolOverlayViewModel;
     ILineOverlayHandler IDocument.LineToolOverlayHandler => LineToolOverlayViewModel;
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
     IAnimationHandler IDocument.AnimationHandler => AnimationDataViewModel;
     IAnimationHandler IDocument.AnimationHandler => AnimationDataViewModel;
-    public bool UsesLegacyBlending { get; private set; }
+    public bool UsesSrgbBlending { get; private set; }
 
 
     private DocumentViewModel()
     private DocumentViewModel()
     {
     {
@@ -318,12 +318,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         var acc = viewModel.Internals.ActionAccumulator;
         var acc = viewModel.Internals.ActionAccumulator;
 
 
         ColorSpace targetProcessingColorSpace = ColorSpace.CreateSrgbLinear();
         ColorSpace targetProcessingColorSpace = ColorSpace.CreateSrgbLinear();
-        if (builderInstance.UsesLegacyColorBlending ||
-            IsFileWithOldColorBlending(serializerData, builderInstance.PixiParserVersionUsed))
+        if (builderInstance.UsesSrgbColorBlending ||
+            IsFileWithSrgbColorBlending(serializerData, builderInstance.PixiParserVersionUsed))
         {
         {
             targetProcessingColorSpace = ColorSpace.CreateSrgb();
             targetProcessingColorSpace = ColorSpace.CreateSrgb();
             viewModel.Internals.Tracker.Document.InitProcessingColorSpace(ColorSpace.CreateSrgb());
             viewModel.Internals.Tracker.Document.InitProcessingColorSpace(ColorSpace.CreateSrgb());
-            viewModel.UsesLegacyBlending = true;
+            viewModel.UsesSrgbBlending = true;
         }
         }
 
 
         viewModel.Internals.ChangeController.SymmetryDraggedInlet(
         viewModel.Internals.ChangeController.SymmetryDraggedInlet(
@@ -486,7 +486,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             }
             }
         }
         }
 
 
-        bool IsFileWithOldColorBlending((string serializerName, string serializerVersion) serializerData,
+        bool IsFileWithSrgbColorBlending((string serializerName, string serializerVersion) serializerData,
             Version? pixiParserVersionUsed)
             Version? pixiParserVersionUsed)
         {
         {
             if (pixiParserVersionUsed != null && pixiParserVersionUsed.Major < 5)
             if (pixiParserVersionUsed != null && pixiParserVersionUsed.Major < 5)
@@ -813,7 +813,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
     public void SetProcessingColorSpace(ColorSpace infoColorSpace)
     public void SetProcessingColorSpace(ColorSpace infoColorSpace)
     {
     {
-        UsesLegacyBlending = infoColorSpace.IsSrgb;
+        UsesSrgbBlending = infoColorSpace.IsSrgb;
     }
     }
 
 
     public void SetSize(VecI size)
     public void SetSize(VecI size)

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit 5b632b4c649f8e88ec8616a383fac56afd728e34
+Subproject commit f3475837fbd05e8ba11bcc5f4fc8cca1458d73d9