Browse Source

Added export frames and quality

Krzysztof Krysiński 2 months ago
parent
commit
8e843c243d

+ 9 - 0
src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs

@@ -7,3 +7,12 @@ public interface IAnimationRenderer
     public Task<bool> RenderAsync(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
     public Task<bool> RenderAsync(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
     public bool Render(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
     public bool Render(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
 }
 }
+
+public enum QualityPreset
+{
+    VeryLow = 0,
+    Low = 1,
+    Medium = 2,
+    High = 3,
+    VeryHigh = 4,
+}

+ 11 - 1
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -17,6 +17,7 @@ public class FFMpegRenderer : IAnimationRenderer
     public int FrameRate { get; set; } = 60;
     public int FrameRate { get; set; } = 60;
     public string OutputFormat { get; set; } = "mp4";
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
     public VecI Size { get; set; }
+    public QualityPreset QualityPreset { get; set; } = QualityPreset.VeryHigh;
 
 
     public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
     public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
         Action<double>? progressCallback = null)
         Action<double>? progressCallback = null)
@@ -215,11 +216,20 @@ public class FFMpegRenderer : IAnimationRenderer
 
 
     private FFMpegArgumentProcessor GetMp4Arguments(FFMpegArguments args, string outputPath)
     private FFMpegArgumentProcessor GetMp4Arguments(FFMpegArguments args, string outputPath)
     {
     {
+        int qscale = QualityPreset switch
+        {
+            QualityPreset.VeryLow => 31,
+            QualityPreset.Low => 25,
+            QualityPreset.Medium => 19,
+            QualityPreset.High => 10,
+            QualityPreset.VeryHigh => 1,
+            _ => 2
+        };
         return args
         return args
             .OutputToFile(outputPath, true, options =>
             .OutputToFile(outputPath, true, options =>
             {
             {
                 options.WithFramerate(FrameRate)
                 options.WithFramerate(FrameRate)
-                    .WithVideoBitrate(1800)
+                    .WithCustomArgument($"-qscale:v {qscale}")
                     .WithVideoCodec("mpeg4")
                     .WithVideoCodec("mpeg4")
                     .ForcePixelFormat("yuv420p");
                     .ForcePixelFormat("yuv420p");
             });
             });

+ 8 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1059,5 +1059,12 @@
   "STEP_START": "Step back to closest cel",
   "STEP_START": "Step back to closest cel",
   "STEP_END": "Step forward to closest cel",
   "STEP_END": "Step forward to closest cel",
   "STEP_FORWARD": "Step forward one frame",
   "STEP_FORWARD": "Step forward one frame",
-  "STEP_BACK": "Step back one frame"
+  "STEP_BACK": "Step back one frame",
+  "ANIMATION_QUALITY_PRESET": "Quality Preset",
+  "VERY_LOW_QUALITY_PRESET": "Very Low",
+  "LOW_QUALITY_PRESET": "Low",
+  "MEDIUM_QUALITY_PRESET": "Medium",
+  "HIGH_QUALITY_PRESET": "High",
+  "VERY_HIGH_QUALITY_PRESET": "Very High",
+  "EXPORT_FRAMES": "Export Frames"
 }
 }

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

@@ -14,6 +14,7 @@ public class ExportConfig
    
    
    public VectorExportConfig? VectorExportConfig { get; set; }
    public VectorExportConfig? VectorExportConfig { get; set; }
    public string ExportOutput { get; set; }
    public string ExportOutput { get; set; }
+   public bool ExportFramesToFolder { get; set; }
 
 
    public ExportConfig(VecI exportSize)
    public ExportConfig(VecI exportSize)
    {
    {

+ 54 - 0
src/PixiEditor/Models/IO/Exporter.cs

@@ -5,10 +5,12 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 
 
 namespace PixiEditor.Models.IO;
 namespace PixiEditor.Models.IO;
@@ -114,6 +116,23 @@ internal class Exporter
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
             return new SaveResult(SaveResultType.InvalidPath);
             return new SaveResult(SaveResultType.InvalidPath);
 
 
+        if (exportConfig.ExportFramesToFolder)
+        {
+            try
+            {
+                await ExportFramesToFolderAsync(document, directory, exportConfig, job);
+                job?.Finish();
+                return new SaveResult(SaveResultType.Success);
+            }
+            catch (Exception e)
+            {
+                job?.Finish();
+                Console.WriteLine(e);
+                CrashHelper.SendExceptionInfo(e);
+                return new SaveResult(SaveResultType.UnknownError);
+            }
+        }
+
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
 
 
         if (typeFromPath is null)
         if (typeFromPath is null)
@@ -161,6 +180,41 @@ internal class Exporter
         }
         }
     }
     }
 
 
+    private static async Task ExportFramesToFolderAsync(DocumentViewModel document, string directory,
+        ExportConfig exportConfig, ExportJob? job)
+    {
+        if (!Directory.Exists(directory))
+        {
+            Directory.CreateDirectory(directory);
+        }
+
+        int totalFrames = document.AnimationDataViewModel.GetVisibleFramesCount();
+        document.RenderFramesProgressive(
+            (surface, frame) =>
+        {
+            job?.CancellationTokenSource.Token.ThrowIfCancellationRequested();
+            job?.Report(((double)frame / totalFrames),
+                new LocalizedString("RENDERING_FRAME", frame, totalFrames));
+            if (exportConfig.ExportSize != surface.Size)
+            {
+                var resized = surface.ResizeNearestNeighbor(exportConfig.ExportSize);
+                SaveAsPng(Path.Combine(directory, $"{frame}.png"), resized);
+            }
+            else
+            {
+                SaveAsPng(Path.Combine(directory, $"{frame}.png"), surface);
+            }
+
+        }, CancellationToken.None, exportConfig.ExportOutput);
+    }
+
+    public static void SaveAsPng(string path, Surface surface)
+    {
+        using var snapshot = surface.DrawingSurface.Snapshot();
+        using var fileStream = new FileStream(path, FileMode.Create);
+        snapshot.Encode(EncodedImageFormat.Png).SaveTo(fileStream);
+    }
+
     public static void SaveAsGZippedBytes(string path, Surface surface)
     public static void SaveAsGZippedBytes(string path, Surface surface)
     {
     {
         SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));
         SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));

+ 5 - 2
src/PixiEditor/Views/Dialogs/ExportFileDialog.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 
 
 namespace PixiEditor.Views.Dialogs;
 namespace PixiEditor.Views.Dialogs;
@@ -143,14 +144,16 @@ internal class ExportFileDialog : CustomDialog
             FilePath = popup.SavePath;
             FilePath = popup.SavePath;
             ChosenFormat = popup.SaveFormat;
             ChosenFormat = popup.SaveFormat;
             ExportOutput = popup.ExportOutput;
             ExportOutput = popup.ExportOutput;
-            
+
             ExportConfig.ExportSize = new VecI(FileWidth, FileHeight);
             ExportConfig.ExportSize = new VecI(FileWidth, FileHeight);
             ExportConfig.ExportOutput = ExportOutput.Name;
             ExportConfig.ExportOutput = ExportOutput.Name;
+            ExportConfig.ExportFramesToFolder = popup.FolderExport;
             ExportConfig.AnimationRenderer = ChosenFormat is VideoFileType ? new FFMpegRenderer()
             ExportConfig.AnimationRenderer = ChosenFormat is VideoFileType ? new FFMpegRenderer()
             {
             {
                 Size = new VecI(FileWidth, FileHeight),
                 Size = new VecI(FileWidth, FileHeight),
                 OutputFormat = ChosenFormat.PrimaryExtension.Replace(".", ""),
                 OutputFormat = ChosenFormat.PrimaryExtension.Replace(".", ""),
-                FrameRate = document.AnimationDataViewModel.FrameRateBindable
+                FrameRate = document.AnimationDataViewModel.FrameRateBindable,
+                QualityPreset = (QualityPreset)popup.AnimationPresetIndex
             }
             }
             : null;
             : null;
             ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;
             ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;

+ 21 - 1
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml

@@ -7,6 +7,7 @@
                          xmlns:indicators="clr-namespace:PixiEditor.Views.Indicators"
                          xmlns:indicators="clr-namespace:PixiEditor.Views.Indicators"
                          xmlns:input1="clr-namespace:PixiEditor.Views.Input"
                          xmlns:input1="clr-namespace:PixiEditor.Views.Input"
                          xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
                          xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
+                         xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
                          CanResize="False"
                          CanResize="False"
                          CanMinimize="False"
                          CanMinimize="False"
                          SizeToContent="WidthAndHeight"
                          SizeToContent="WidthAndHeight"
@@ -27,7 +28,26 @@
                 </TabControl.Styles>
                 </TabControl.Styles>
                 <TabControl.Items>
                 <TabControl.Items>
                     <TabItem IsSelected="True" ui1:Translator.Key="EXPORT_IMAGE_HEADER" />
                     <TabItem IsSelected="True" ui1:Translator.Key="EXPORT_IMAGE_HEADER" />
-                    <TabItem ui1:Translator.Key="EXPORT_ANIMATION_HEADER" />
+                    <TabItem ui1:Translator.Key="EXPORT_ANIMATION_HEADER">
+                        <StackPanel Orientation="Vertical" Spacing="5">
+                            <StackPanel Spacing="5" Orientation="Horizontal">
+                                <TextBlock ui1:Translator.Key="ANIMATION_QUALITY_PRESET" />
+                                <ComboBox
+                                    SelectedIndex="{Binding ElementName=saveFilePopup, Path=AnimationPresetIndex}"
+                                    ItemsSource="{Binding ElementName=saveFilePopup, Path=QualityPresetValues}">
+                                    <ComboBox.ItemTemplate>
+                                        <DataTemplate>
+                                            <TextBlock
+                                                ui1:Translator.Key="{Binding Converter={converters:EnumToLocalizedStringConverter}}" />
+                                        </DataTemplate>
+                                    </ComboBox.ItemTemplate>
+                                </ComboBox>
+
+                                <CheckBox IsChecked="{Binding ElementName=saveFilePopup, Path=FolderExport, Mode=TwoWay}"
+                                          ui1:Translator.Key="EXPORT_FRAMES" />
+                            </StackPanel>
+                        </StackPanel>
+                    </TabItem>
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                         <Grid>
                         <Grid>
                             <Grid.ColumnDefinitions>
                             <Grid.ColumnDefinitions>

+ 73 - 24
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -13,6 +13,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 using Image = Drawie.Backend.Core.Surfaces.ImageData.Image;
 using Image = Drawie.Backend.Core.Surfaces.ImageData.Image;
@@ -76,6 +77,14 @@ internal partial class ExportFilePopup : PixiEditorPopup
     public static readonly StyledProperty<string> SizeHintProperty = AvaloniaProperty.Register<ExportFilePopup, string>(
     public static readonly StyledProperty<string> SizeHintProperty = AvaloniaProperty.Register<ExportFilePopup, string>(
         nameof(SizeHint));
         nameof(SizeHint));
 
 
+    public static readonly StyledProperty<bool> FolderExportProperty = AvaloniaProperty.Register<ExportFilePopup, bool>(
+        nameof(FolderExport));
+
+    public bool FolderExport
+    {
+        get => GetValue(FolderExportProperty);
+        set => SetValue(FolderExportProperty, value);
+    }
     public string SizeHint
     public string SizeHint
     {
     {
         get => GetValue(SizeHintProperty);
         get => GetValue(SizeHintProperty);
@@ -171,6 +180,14 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
 
     public bool IsSpriteSheetExport => SelectedExportIndex == 2;
     public bool IsSpriteSheetExport => SelectedExportIndex == 2;
 
 
+    public int AnimationPresetIndex
+    {
+        get { return (int)GetValue(AnimationPresetIndexProperty); }
+        set { SetValue(AnimationPresetIndexProperty, value); }
+    }
+
+    public Array QualityPresetValues { get; }
+
     private DocumentViewModel document;
     private DocumentViewModel document;
     private Image[]? videoPreviewFrames = [];
     private Image[]? videoPreviewFrames = [];
     private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
     private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
@@ -179,6 +196,9 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
 
     private Task? generateSpriteSheetTask;
     private Task? generateSpriteSheetTask;
 
 
+    public static readonly StyledProperty<int> AnimationPresetIndexProperty
+        = AvaloniaProperty.Register<ExportFilePopup, int>("AnimationPresetIndex", 4);
+
     static ExportFilePopup()
     static ExportFilePopup()
     {
     {
         SaveWidthProperty.Changed.Subscribe(RerenderPreview);
         SaveWidthProperty.Changed.Subscribe(RerenderPreview);
@@ -193,8 +213,10 @@ internal partial class ExportFilePopup : PixiEditorPopup
     {
     {
         SaveWidth = imageWidth;
         SaveWidth = imageWidth;
         SaveHeight = imageHeight;
         SaveHeight = imageHeight;
+        QualityPresetValues = Enum.GetValues(typeof(QualityPreset));
 
 
         InitializeComponent();
         InitializeComponent();
+
         DataContext = this;
         DataContext = this;
         Loaded += (_, _) => sizePicker.FocusWidthPicker();
         Loaded += (_, _) => sizePicker.FocusWidthPicker();
 
 
@@ -467,37 +489,64 @@ internal partial class ExportFilePopup : PixiEditorPopup
     /// </summary>
     /// </summary>
     private async Task<string?> ChoosePath()
     private async Task<string?> ChoosePath()
     {
     {
-        FilePickerSaveOptions options = new FilePickerSaveOptions
-        {
-            Title = new LocalizedString("EXPORT_SAVE_TITLE"),
-            SuggestedFileName = SuggestedName,
-            SuggestedStartLocation = string.IsNullOrEmpty(document.FullFilePath)
-                ? await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
-                : await GetTopLevel(this).StorageProvider.TryGetFolderFromPathAsync(document.FullFilePath),
-            FileTypeChoices =
-                SupportedFilesHelper.BuildSaveFilter(SelectedExportIndex == 1
-                    ? FileTypeDialogDataSet.SetKind.Video
-                    : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector),
-            ShowOverwritePrompt = true
-        };
+        bool folderExport = FolderExport && SelectedExportIndex == 1;
 
 
-        IStorageFile file = await GetTopLevel(this).StorageProvider.SaveFilePickerAsync(options);
-        if (file != null)
+        if (folderExport)
         {
         {
-            if (string.IsNullOrEmpty(file.Name) == false)
+            FolderPickerOpenOptions options = new FolderPickerOpenOptions()
             {
             {
-                SaveFormat = SupportedFilesHelper.GetSaveFileType(
-                    SelectedExportIndex == 1
-                        ? FileTypeDialogDataSet.SetKind.Video
-                        : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector, file);
-                if (SaveFormat == null)
+                Title = new LocalizedString("EXPORT_SAVE_TITLE"),
+                SuggestedStartLocation = string.IsNullOrEmpty(document.FullFilePath)
+                    ? await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
+                    : await GetTopLevel(this).StorageProvider.TryGetFolderFromPathAsync(document.FullFilePath),
+                AllowMultiple = false,
+            };
+
+            var folders = await GetTopLevel(this).StorageProvider.OpenFolderPickerAsync(options);
+            if (folders.Count > 0)
+            {
+                IStorageFolder folder = folders[0];
+                if (folder != null)
                 {
                 {
-                    return null;
+                    SavePath = folder.Path.LocalPath;
+                    return SavePath;
                 }
                 }
+            }
+        }
+        else
+        {
+            FilePickerSaveOptions options = new FilePickerSaveOptions
+            {
+                Title = new LocalizedString("EXPORT_SAVE_TITLE"),
+                SuggestedFileName = SuggestedName,
+                SuggestedStartLocation = string.IsNullOrEmpty(document.FullFilePath)
+                    ? await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
+                    : await GetTopLevel(this).StorageProvider.TryGetFolderFromPathAsync(document.FullFilePath),
+                FileTypeChoices =
+                    SupportedFilesHelper.BuildSaveFilter(SelectedExportIndex == 1
+                        ? FileTypeDialogDataSet.SetKind.Video
+                        : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector),
+                ShowOverwritePrompt = true
+            };
 
 
-                string fileName = SupportedFilesHelper.FixFileExtension(file.Path.LocalPath, SaveFormat);
+            IStorageFile file = await GetTopLevel(this).StorageProvider.SaveFilePickerAsync(options);
+            if (file != null)
+            {
+                if (string.IsNullOrEmpty(file.Name) == false)
+                {
+                    SaveFormat = SupportedFilesHelper.GetSaveFileType(
+                        SelectedExportIndex == 1
+                            ? FileTypeDialogDataSet.SetKind.Video
+                            : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector, file);
+                    if (SaveFormat == null)
+                    {
+                        return null;
+                    }
+
+                    string fileName = SupportedFilesHelper.FixFileExtension(file.Path.LocalPath, SaveFormat);
 
 
-                return fileName;
+                    return fileName;
+                }
             }
             }
         }
         }