浏览代码

Implement resize image

Equbuxu 3 年之前
父节点
当前提交
90b0df177b

+ 0 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -575,7 +575,6 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
             if (operation.IgnoreEmptyChunks)
                 chunks.IntersectWith(FindAllChunks());
-            chunks.UnionWith(op.FindAffectedChunks());
             EnqueueOperation(op, chunks);
         }
     }

+ 18 - 0
src/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -78,4 +78,22 @@ public class ChunkyImageTests
         image.Dispose();
         Assert.Equal(0, Chunk.ChunkCounter);
     }
+
+    [Fact]
+    public void EnqueueDrawRectangle_OutsideOfImage_PartsAreNotDrawn()
+    {
+        const int chunkSize = ChunkyImage.FullChunkSize;
+        using ChunkyImage image = new(new VecI(chunkSize));
+        image.EnqueueDrawRectangle(new ShapeData(
+                VecD.Zero,
+                new VecD(chunkSize * 10),
+                0,
+                0,
+                SKColors.Transparent,
+                SKColors.Red));
+        image.CommitChanges();
+        Assert.Collection(
+            image.FindAllChunks(), 
+            elem => Assert.Equal(VecI.Zero, elem));
+    }
 }

+ 32 - 37
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -22,7 +22,7 @@ internal class ResizeCanvas_Change : Change
 
     public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        if (target.Size == newSize)
+        if (target.Size == newSize || newSize.X < 1 || newSize.Y < 1)
             return new Error();
         originalSize = target.Size;
         originalHorAxisY = target.HorizontalSymmetryAxisY;
@@ -30,42 +30,31 @@ internal class ResizeCanvas_Change : Change
         return new Success();
     }
 
-    private void ForEachLayer(Folder folder, Action<Layer> action)
-    {
-        foreach (var child in folder.Children)
-        {
-            if (child is Layer layer)
-            {
-                action(layer);
-            }
-            else if (child is Folder innerFolder)
-                ForEachLayer(innerFolder, action);
-        }
-    }
-
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         target.Size = newSize;
         target.VerticalSymmetryAxisX = Math.Clamp(originalVerAxisX, 0, target.Size.X);
         target.HorizontalSymmetryAxisY = Math.Clamp(originalHorAxisY, 0, target.Size.Y);
 
-        ForEachLayer(target.StructureRoot, (layer) =>
+        target.ForEveryMember((member) =>
         {
-            layer.LayerImage.EnqueueResize(newSize);
-            layer.LayerImage.EnqueueClear();
-            layer.LayerImage.EnqueueDrawChunkyImage(anchor.FindOffsetFor(originalSize, newSize), layer.LayerImage);
-
-            deletedChunks.Add(layer.GuidValue, new CommittedChunkStorage(layer.LayerImage, layer.LayerImage.FindAffectedChunks()));
-            layer.LayerImage.CommitChanges();
+            if (member is Layer layer)
+            {
+                layer.LayerImage.EnqueueResize(newSize);
+                layer.LayerImage.EnqueueClear();
+                layer.LayerImage.EnqueueDrawChunkyImage(anchor.FindOffsetFor(originalSize, newSize), layer.LayerImage);
 
-            if (layer.Mask is null)
+                deletedChunks.Add(layer.GuidValue, new CommittedChunkStorage(layer.LayerImage, layer.LayerImage.FindAffectedChunks()));
+                layer.LayerImage.CommitChanges();
+            }
+            if (member.Mask is null)
                 return;
 
-            layer.Mask.EnqueueResize(newSize);
-            layer.Mask.EnqueueClear();
-            layer.Mask.EnqueueDrawChunkyImage(anchor.FindOffsetFor(originalSize, newSize), layer.Mask);
-            deletedMaskChunks.Add(layer.GuidValue, new CommittedChunkStorage(layer.Mask, layer.Mask.FindAffectedChunks()));
-            layer.Mask.CommitChanges();
+            member.Mask.EnqueueResize(newSize);
+            member.Mask.EnqueueClear();
+            member.Mask.EnqueueDrawChunkyImage(anchor.FindOffsetFor(originalSize, newSize), member.Mask);
+            deletedMaskChunks.Add(member.GuidValue, new CommittedChunkStorage(member.Mask, member.Mask.FindAffectedChunks()));
+            member.Mask.CommitChanges();
         });
         ignoreInUndo = false;
         return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
@@ -74,18 +63,20 @@ internal class ResizeCanvas_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         target.Size = originalSize;
-        ForEachLayer(target.StructureRoot, (layer) =>
+        target.ForEveryMember((member) =>
         {
-            layer.LayerImage.EnqueueResize(originalSize);
-            deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
-            layer.LayerImage.CommitChanges();
-
-            if (layer.Mask is null)
+            if (member is Layer layer)
+            {
+                layer.LayerImage.EnqueueResize(originalSize);
+                deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
+                layer.LayerImage.CommitChanges();
+            }
+            
+            if (member.Mask is null)
                 return;
-
-            layer.Mask.EnqueueResize(originalSize);
-            deletedMaskChunks[layer.GuidValue].ApplyChunksToImage(layer.Mask);
-            layer.Mask.CommitChanges();
+            member.Mask.EnqueueResize(originalSize);
+            deletedMaskChunks[member.GuidValue].ApplyChunksToImage(member.Mask);
+            member.Mask.CommitChanges();
         });
 
         target.HorizontalSymmetryAxisY = originalHorAxisY;
@@ -94,6 +85,10 @@ internal class ResizeCanvas_Change : Change
         foreach (var stored in deletedChunks)
             stored.Value.Dispose();
         deletedChunks = new();
+        
+        foreach (var stored in deletedMaskChunks)
+            stored.Value.Dispose();
+        deletedMaskChunks = new();
 
         return new Size_ChangeInfo(originalSize, originalVerAxisX, originalHorAxisY);
     }

+ 145 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -0,0 +1,145 @@
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+
+internal class ResizeImage_Change : Change
+{
+    private readonly VecI newSize;
+    private readonly ResamplingMethod method;
+    private VecI originalSize;
+    private int originalHorAxisY;
+    private int originalVerAxisX;
+    
+    private Dictionary<Guid, CommittedChunkStorage> savedChunks = new();
+    private Dictionary<Guid, CommittedChunkStorage> savedMaskChunks = new();
+
+    [GenerateMakeChangeAction]
+    public ResizeImage_Change(VecI size, ResamplingMethod method)
+    {
+        this.newSize = size;
+        this.method = method;
+    }
+    
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (target.Size == newSize || newSize.X < 1 || newSize.Y < 1)
+            return new Error();
+        
+        originalSize = target.Size;
+        originalHorAxisY = target.HorizontalSymmetryAxisY;
+        originalVerAxisX = target.VerticalSymmetryAxisX;
+        return new Success();
+    }
+
+    private static SKFilterQuality ToFilterQuality(ResamplingMethod method, bool downscaling) =>
+        (method, downscaling) switch
+        {
+            (ResamplingMethod.NearestNeighbor, _) => SKFilterQuality.None,
+            (ResamplingMethod.Bilinear, true) => SKFilterQuality.Medium,
+            (ResamplingMethod.Bilinear, false) => SKFilterQuality.Low,
+            (ResamplingMethod.Bicubic, _) => SKFilterQuality.High,
+            _ => throw new ArgumentOutOfRangeException(),
+        };
+
+    private void ScaleChunkyImage(ChunkyImage image)
+    {
+        using Surface originalSurface = new(originalSize);
+        image.DrawMostUpToDateRegionOn(
+            new(VecI.Zero, originalSize), 
+            ChunkResolution.Full,
+            originalSurface.SkiaSurface,
+            VecI.Zero);
+        
+        bool downscaling = newSize.LengthSquared < originalSize.LengthSquared;
+        SKFilterQuality quality = ToFilterQuality(method, downscaling);
+        using SKPaint paint = new()
+        {
+            FilterQuality = quality, 
+            BlendMode = SKBlendMode.Src,
+        };
+
+        using Surface newSurface = new(newSize);
+        newSurface.SkiaSurface.Canvas.Save();
+        newSurface.SkiaSurface.Canvas.Scale(newSize.X / (float)originalSize.X, newSize.Y / (float)originalSize.Y);
+        newSurface.SkiaSurface.Canvas.DrawSurface(originalSurface.SkiaSurface, 0, 0, paint);
+        newSurface.SkiaSurface.Canvas.Restore();
+        
+        image.EnqueueResize(newSize);
+        image.EnqueueClear();
+        image.EnqueueDrawImage(VecI.Zero, newSurface);
+    }
+    
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        target.Size = newSize;
+        target.VerticalSymmetryAxisX = Math.Clamp(originalVerAxisX, 0, target.Size.X);
+        target.HorizontalSymmetryAxisY = Math.Clamp(originalHorAxisY, 0, target.Size.Y);
+
+        target.ForEveryMember(member =>
+        {
+            if (member is Layer layer)
+            {
+                ScaleChunkyImage(layer.LayerImage);
+                var affected = layer.LayerImage.FindAffectedChunks();
+                savedChunks[layer.GuidValue] = new CommittedChunkStorage(layer.LayerImage, affected);
+                layer.LayerImage.CommitChanges();
+            }
+            if (member.Mask is not null)
+            {
+                ScaleChunkyImage(member.Mask);
+                var affected = member.Mask.FindAffectedChunks();
+                savedMaskChunks[member.GuidValue] = new CommittedChunkStorage(member.Mask, affected);
+                member.Mask.CommitChanges();
+            }
+        });
+
+        ignoreInUndo = false;
+        return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        target.Size = originalSize;
+        target.ForEveryMember((member) =>
+        {
+            if (member is Layer layer)
+            {
+                layer.LayerImage.EnqueueResize(originalSize);
+                layer.LayerImage.EnqueueClear();
+                savedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
+                layer.LayerImage.CommitChanges();
+            }
+            
+            if (member.Mask is not null)
+            {
+                member.Mask.EnqueueResize(originalSize);
+                member.Mask.EnqueueClear();
+                savedMaskChunks[member.GuidValue].ApplyChunksToImage(member.Mask);
+                member.Mask.CommitChanges();
+            }
+        });
+
+        target.HorizontalSymmetryAxisY = originalHorAxisY;
+        target.VerticalSymmetryAxisX = originalVerAxisX;
+
+        foreach (var stored in savedChunks)
+            stored.Value.Dispose();
+        savedChunks = new();
+        
+        foreach (var stored in savedMaskChunks)
+            stored.Value.Dispose();
+        savedMaskChunks = new();
+
+        return new Size_ChangeInfo(originalSize, originalVerAxisX, originalHorAxisY);
+    }
+
+    public override void Dispose()
+    {
+        foreach (var layer in savedChunks)
+            layer.Value.Dispose();
+        foreach (var mask in savedMaskChunks)
+            mask.Value.Dispose();
+    }
+}

+ 8 - 0
src/PixiEditor.ChangeableDocument/Enums/ResamplingMethod.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.ChangeableDocument.Enums;
+
+public enum ResamplingMethod
+{
+    NearestNeighbor,
+    Bilinear,
+    Bicubic,
+}

+ 9 - 0
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -30,6 +30,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public RelayCommand? CreateNewFolderCommand { get; }
     public RelayCommand? DeleteStructureMemberCommand { get; }
     public RelayCommand? ResizeCanvasCommand { get; }
+    public RelayCommand? ResizeImageCommand { get; }
     public RelayCommand? CombineCommand { get; }
     public RelayCommand? ClearHistoryCommand { get; }
     public RelayCommand? CreateMaskCommand { get; }
@@ -211,6 +212,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
         CreateNewFolderCommand = new RelayCommand(_ => Helpers.StructureHelper.CreateNewStructureMember(StructureMemberType.Folder));
         DeleteStructureMemberCommand = new RelayCommand(DeleteStructureMember);
         ResizeCanvasCommand = new RelayCommand(ResizeCanvas);
+        ResizeImageCommand = new RelayCommand(ResizeImage);
         CombineCommand = new RelayCommand(Combine);
         ClearHistoryCommand = new RelayCommand(ClearHistory);
         CreateMaskCommand = new RelayCommand(CreateMask);
@@ -238,6 +240,13 @@ internal class DocumentViewModel : INotifyPropertyChanged
         PreviewSurface = SKSurface.Create(new SKImageInfo(previewSize.X, previewSize.Y, SKColorType.Bgra8888), PreviewBitmap.BackBuffer, PreviewBitmap.BackBufferStride);
     }
 
+    private void ResizeImage(object? obj)
+    {
+        if (updateableChangeActive)
+            return;
+        Helpers.ActionAccumulator.AddFinishedActions(new ResizeImage_Action(new(ResizeWidth, ResizeHeight), ResamplingMethod.NearestNeighbor));
+    }
+
     public static DocumentViewModel FromSerializableDocument(ViewModelMain owner, SerializableDocument serDocument, string name)
     {
         DocumentViewModel document = new DocumentViewModel(owner, name);

+ 8 - 2
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -580,10 +580,16 @@
                     Margin="5"
                     Text="{Binding ActiveDocument.ResizeHeight}" />
                 <Button
-                    Width="50"
+                    Width="80"
                     Margin="5"
                     Command="{Binding ActiveDocument.ResizeCanvasCommand}">
-                    Resize
+                    Resize Canvas
+                </Button>
+                <Button
+                    Width="80"
+                    Margin="5"
+                    Command="{Binding ActiveDocument.ResizeImageCommand}">
+                    Resize Image
                 </Button>
             </WrapPanel>
         </Border>

+ 1 - 1
src/README.md

@@ -89,7 +89,7 @@ Decouples the state of a document from the UI.
         - [ ] Layer/Folder locking
         - [ ] Reference layer manipulation?
         - [x] Resize canvas
-        - [ ] Resize image
+        - [x] Resize image
         - [x] Paste image with transformation
         - [x] Rectangle
         - [x] Ellipse