Browse Source

Fix #403 and fix exporting using lazy bitmaps, also rewrite a lot of exporting code by accident

Equbuxu 2 years ago
parent
commit
a2b4698c19

+ 84 - 76
src/PixiEditor/Models/IO/Exporter.cs

@@ -6,8 +6,10 @@ using System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
@@ -15,70 +17,102 @@ using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Models.IO;
 
+internal enum DialogSaveResult
+{
+    Success = 0,
+    InvalidPath = 1,
+    ConcurrencyError = 2,
+    UnknownError = 3,
+    Cancelled = 4,
+}
+
+internal enum SaveResult
+{
+    Success = 0,
+    InvalidPath = 1,
+    ConcurrencyError = 2,
+    UnknownError = 3,
+}
+
 internal class Exporter
 {
     /// <summary>
-    ///     Saves document as .pixi file that contains all document data.
+    /// Attempts to save file using a SaveFileDialog
     /// </summary>
-    /// <param name="document">Document to save.</param>
-    /// <param name="path">Path where file was saved.</param>
-    public static bool SaveAsEditableFileWithDialog(DocumentViewModel document, out string path)
+    public static DialogSaveResult TrySaveWithDialog(DocumentViewModel document, out string path, VecI? exportSize = null)
     {
+        path = "";
         SaveFileDialog dialog = new SaveFileDialog
         {
             Filter = SupportedFilesHelper.BuildSaveFilter(true),
             FilterIndex = 0,
             DefaultExt = "pixi"
-
         };
-        if ((bool)dialog.ShowDialog())
-        {
-            FileType filetype = SupportedFilesHelper.GetSaveFileTypeFromFilterIndex(true, dialog.FilterIndex);
-            path = SaveAsEditableFile(document, dialog.FileName, filetype);
-            return true;
-        }
 
-        path = string.Empty;
-        return false;
+        bool? result = dialog.ShowDialog();
+        if (result is null || result == false)
+            return DialogSaveResult.Cancelled;
+
+        var fileType = SupportedFilesHelper.GetSaveFileTypeFromFilterIndex(true, dialog.FilterIndex);
+
+        var saveResult = TrySaveUsingDataFromDialog(document, dialog.FileName, fileType, out string fixedPath, exportSize);
+        if (saveResult == SaveResult.Success)
+            path = fixedPath;
+
+        return (DialogSaveResult)saveResult;
     }
 
     /// <summary>
-    /// Saves editable file to chosen path and returns it.
+    /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// </summary>
-    /// <param name="document">Document to be saved.</param>
-    /// <param name="path">Path where to save file.</param>
-    /// <returns>Path.</returns>
-    public static string SaveAsEditableFile(DocumentViewModel document, string path, FileType requestedType = FileType.Unset)
+    public static SaveResult TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, FileType fileTypeFromDialog, out string finalPath, VecI? exportSize = null)
     {
-        var typeFromPath = ParseImageFormat(Path.GetExtension(path));
-        FileType finalType = (typeFromPath, requestedType) switch
-        {
-            (FileType.Unset, FileType.Unset) => FileType.Pixi,
-            (var first, FileType.Unset) => first,
-            (FileType.Unset, var second) => second,
-            _ => typeFromPath,
-        };
+        finalPath = FixFileExtension(pathFromDialog, fileTypeFromDialog);
+        var saveResult = TrySave(document, finalPath, exportSize);
+        if (saveResult != SaveResult.Success)
+            finalPath = "";
 
-        if (typeFromPath == FileType.Unset)
-        {
-            path = AppendExtension(path, SupportedFilesHelper.GetFileTypeDialogData(finalType));
-        }
+        return saveResult;
+    }
 
-        if (finalType != FileType.Pixi)
-        {
-            var bitmap = document.LazyBitmaps[ChunkResolution.Full];
-            SaveAs(encodersFactory[finalType](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
-        }
-        else if (Directory.Exists(Path.GetDirectoryName(path)))
+    private static string FixFileExtension(string pathWithOrWithoutExtension, FileType requestedType)
+    {
+        if (requestedType == FileType.Unset)
+            throw new ArgumentException("A valid filetype is required", nameof(requestedType));
+
+        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
+        if (typeFromPath != FileType.Unset && typeFromPath == requestedType)
+            return pathWithOrWithoutExtension;
+        return AppendExtension(pathWithOrWithoutExtension, SupportedFilesHelper.GetFileTypeDialogData(requestedType));
+    }
+
+    /// <summary>
+    /// Attempts to save the document into the given location, filetype is inferred from path
+    /// </summary>
+    public static SaveResult TrySave(DocumentViewModel document, string pathWithExtension, VecI? exportSize = null)
+    {
+        string directory = Path.GetDirectoryName(pathWithExtension);
+        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+            return SaveResult.InvalidPath;
+
+        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
+
+        if (typeFromPath != FileType.Pixi)
         {
-            Parser.PixiParser.Serialize(document.ToSerializable(), path);
+            var maybeBitmap = document.MaybeRenderWholeImage();
+            if (maybeBitmap.IsT0)
+                return SaveResult.ConcurrencyError;
+            var bitmap = maybeBitmap.AsT1;
+
+            if (!TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize))
+                return SaveResult.UnknownError;
         }
         else
         {
-            SaveAsEditableFileWithDialog(document, out path);
+            Parser.PixiParser.Serialize(document.ToSerializable(), pathWithExtension);
         }
 
-        return path;
+        return SaveResult.Success;
     }
 
     private static string AppendExtension(string path, FileTypeDialogData data)
@@ -91,11 +125,6 @@ internal class Exporter
         return Path.Combine(Path.GetDirectoryName(path), filename);
     }
 
-    public static FileType ParseImageFormat(string extension)
-    {
-        return SupportedFilesHelper.ParseImageFormat(extension);
-    }
-
     static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
 
     static Exporter()
@@ -106,28 +135,6 @@ internal class Exporter
         encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
     }
 
-    /// <summary>
-    ///     Creates ExportFileDialog to get width, height and path of file.
-    /// </summary>
-    /// <param name="bitmap">Bitmap to be saved as file.</param>
-    /// <param name="fileDimensions">Size of file.</param>
-    public static bool Export(WriteableBitmap bitmap, VecI fileDimensions, out string path)
-    {
-        ExportFileDialog info = new ExportFileDialog(fileDimensions);
-
-        // If OK on dialog has been clicked
-        if (info.ShowDialog())
-        {
-            if (encodersFactory.ContainsKey(info.ChosenFormat))
-                SaveAs(encodersFactory[info.ChosenFormat](), info.FilePath, info.FileWidth, info.FileHeight, bitmap);
-            
-            path = info.FilePath;
-            return true;
-        }
-
-        path = string.Empty;
-        return false;
-    }
     public static void SaveAsGZippedBytes(string path, Surface surface)
     {
         SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));
@@ -156,25 +163,26 @@ internal class Exporter
     }
 
     /// <summary>
-    ///     Saves image to PNG file.
+    /// Saves image to PNG file. Messes with the passed bitmap.
     /// </summary>
-    /// <param name="encoder">encoder to do the job.</param>
-    /// <param name="savePath">Save file path.</param>
-    /// <param name="exportWidth">File width.</param>
-    /// <param name="exportHeight">File height.</param>
-    /// <param name="bitmap">Bitmap to save.</param>
-    private static void SaveAs(BitmapEncoder encoder, string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
+    private static bool TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
     {
         try
         {
-            bitmap = bitmap.Resize(exportWidth, exportHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+            if (exportSize is not null && exportSize != bitmap.Size)
+                bitmap = bitmap.ResizeNearestNeighbor((VecI)exportSize);
+
+            if (encoder is (JpegBitmapEncoder or BmpBitmapEncoder))
+                bitmap.DrawingSurface.Canvas.DrawColor(Colors.White, DrawingApi.Core.Surface.BlendMode.Multiply);
+
             using var stream = new FileStream(savePath, FileMode.Create);
-            encoder.Frames.Add(BitmapFrame.Create(bitmap));
+            encoder.Frames.Add(BitmapFrame.Create(bitmap.ToWriteableBitmap()));
             encoder.Save(stream);
         }
         catch (Exception err)
         {
-            NoticeDialog.Show(err.ToString(), "Error");
+            return false;
         }
+        return true;
     }
 }

+ 2 - 2
src/PixiEditor/ViewModels/SaveFilePopupViewModel.cs

@@ -65,7 +65,7 @@ internal class SaveFilePopupViewModel : ViewModelBase
         {
             if (string.IsNullOrEmpty(path.FileName) == false)
             {
-                ChosenFormat = Exporter.ParseImageFormat(Path.GetExtension(path.SafeFileName));
+                ChosenFormat = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(path.SafeFileName));
                 return path.FileName;
             }
         }
@@ -93,4 +93,4 @@ internal class SaveFilePopupViewModel : ViewModelBase
         ((Window)parameter).DialogResult = true;
         CloseButton(parameter);
     }
-}
+}

+ 25 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -278,6 +278,31 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
 
+    public OneOf<Error, Surface> MaybeRenderWholeImage()
+    {
+        try
+        {
+            Surface finalSurface = new Surface(SizeBindable);
+            VecI sizeInChunks = (VecI)((VecD)SizeBindable / ChunkyImage.FullChunkSize).Ceiling();
+            for (int i = 0; i < sizeInChunks.X; i++)
+            {
+                for (int j = 0; j < sizeInChunks.Y; j++)
+                {
+                    var maybeChunk = ChunkRenderer.MergeWholeStructure(new(i, j), ChunkResolution.Full, Internals.Tracker.Document.StructureRoot);
+                    if (maybeChunk.IsT1)
+                        continue;
+                    using Chunk chunk = maybeChunk.AsT0;
+                    finalSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, i * ChunkyImage.FullChunkSize, j * ChunkyImage.FullChunkSize);
+                } 
+            }
+            return finalSurface;
+        }
+        catch (ObjectDisposedException)
+        {
+            return new Error();
+        }
+    }
+
     /// <summary>
     /// Takes the selected area and converts it into a surface
     /// </summary>

+ 45 - 19
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
 using System.Reflection.Metadata;
 using System.Windows.Input;
 using System.Windows.Shapes;
@@ -258,29 +259,34 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
     public bool SaveDocument(DocumentViewModel document, bool asNew)
     {
-        string path = "";
-        bool success = false;
+        string finalPath = null;
         if (asNew || string.IsNullOrEmpty(document.FullFilePath))
         {
-            success = Exporter.SaveAsEditableFileWithDialog(document, out path);
-            if (success)
+            var result = Exporter.TrySaveWithDialog(document, out string path);
+            if (result == DialogSaveResult.Cancelled)
+                return false;
+            if (result != DialogSaveResult.Success)
             {
-                AddRecentlyOpened(path);
+                ShowSaveError(result);
+                return false;
             }
+            finalPath = path;
+            AddRecentlyOpened(path);
         }
         else
         {
-            path = Exporter.SaveAsEditableFile(document, document.FullFilePath);
-            success = path != null;
-        }
-
-        if (success)
-        {
-            document.FullFilePath = path;
-            document.MarkAsSaved();
+            var result = Exporter.TrySave(document, document.FullFilePath);
+            if (result != SaveResult.Success)
+            {
+                ShowSaveError((DialogSaveResult)result);
+                return false;
+            }
+            finalPath = document.FullFilePath;
         }
 
-        return success;
+        document.FullFilePath = finalPath;
+        document.MarkAsSaved();
+        return true;
     }
 
     /// <summary>
@@ -290,15 +296,35 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.File.Export", "Export", "Export image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift)]
     public void ExportFile()
     {
-        ViewModelMain.Current.ActionDisplay = "";
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
+        ViewModelMain.Current.ActionDisplay = "";
 
-        var bitmap = doc.LazyBitmaps[ChunkResolution.Full];
-        if (Exporter.Export(bitmap, new VecI(bitmap.PixelWidth, bitmap.PixelHeight), out string path))
+        ExportFileDialog info = new ExportFileDialog(doc.SizeBindable);
+        if (info.ShowDialog())
+        {
+            SaveResult result = Exporter.TrySaveUsingDataFromDialog(doc, info.FilePath, info.ChosenFormat, out string finalPath, new(info.FileWidth, info.FileHeight));
+            if (result == SaveResult.Success)
+                ProcessHelper.OpenInExplorer(finalPath);
+            else
+                ShowSaveError((DialogSaveResult)result);
+        }
+    }
+
+    private void ShowSaveError(DialogSaveResult result)
+    {
+        switch (result)
         {
-            ProcessHelper.OpenInExplorer(path);
+            case DialogSaveResult.InvalidPath:
+                NoticeDialog.Show("Error", "Couldn't save the file to the specified location");
+                break;
+            case DialogSaveResult.ConcurrencyError:
+                NoticeDialog.Show("Internal error", "An internal error occured while saving. Please try again.");
+                break;
+            case DialogSaveResult.UnknownError:
+                NoticeDialog.Show("Error", "An error occured while saving.");
+                break;
         }
     }