Merge branch 'master' into gradient-fixes
This commit is contained in:
commit
187be9274e
@ -7,3 +7,12 @@ public interface IAnimationRenderer
|
||||
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 enum QualityPreset
|
||||
{
|
||||
VeryLow = 0,
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
VeryHigh = 4,
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ public class FFMpegRenderer : IAnimationRenderer
|
||||
public int FrameRate { get; set; } = 60;
|
||||
public string OutputFormat { get; set; } = "mp4";
|
||||
public VecI Size { get; set; }
|
||||
public QualityPreset QualityPreset { get; set; } = QualityPreset.VeryHigh;
|
||||
|
||||
public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
|
||||
Action<double>? progressCallback = null)
|
||||
@ -215,11 +216,20 @@ public class FFMpegRenderer : IAnimationRenderer
|
||||
|
||||
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
|
||||
.OutputToFile(outputPath, true, options =>
|
||||
{
|
||||
options.WithFramerate(FrameRate)
|
||||
.WithVideoBitrate(1800)
|
||||
.WithCustomArgument($"-qscale:v {qscale}")
|
||||
.WithVideoCodec("mpeg4")
|
||||
.ForcePixelFormat("yuv420p");
|
||||
});
|
||||
|
@ -1059,5 +1059,12 @@
|
||||
"STEP_START": "Step back to closest cel",
|
||||
"STEP_END": "Step forward to closest cel",
|
||||
"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"
|
||||
}
|
@ -14,6 +14,7 @@ public class ExportConfig
|
||||
|
||||
public VectorExportConfig? VectorExportConfig { get; set; }
|
||||
public string ExportOutput { get; set; }
|
||||
public bool ExportFramesToFolder { get; set; }
|
||||
|
||||
public ExportConfig(VecI exportSize)
|
||||
{
|
||||
|
@ -5,10 +5,12 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform.Storage;
|
||||
using ChunkyImageLib;
|
||||
using Drawie.Backend.Core;
|
||||
using Drawie.Backend.Core.Surfaces;
|
||||
using Drawie.Backend.Core.Surfaces.ImageData;
|
||||
using PixiEditor.Helpers;
|
||||
using PixiEditor.Models.Files;
|
||||
using Drawie.Numerics;
|
||||
using PixiEditor.UI.Common.Localization;
|
||||
using PixiEditor.ViewModels.Document;
|
||||
|
||||
namespace PixiEditor.Models.IO;
|
||||
@ -114,6 +116,23 @@ internal class Exporter
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
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));
|
||||
|
||||
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)
|
||||
{
|
||||
SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));
|
||||
|
@ -7,6 +7,7 @@ using PixiEditor.Models.Dialogs;
|
||||
using PixiEditor.Models.Files;
|
||||
using PixiEditor.Models.IO;
|
||||
using Drawie.Numerics;
|
||||
using PixiEditor.AnimationRenderer.Core;
|
||||
using PixiEditor.ViewModels.Document;
|
||||
|
||||
namespace PixiEditor.Views.Dialogs;
|
||||
@ -143,14 +144,16 @@ internal class ExportFileDialog : CustomDialog
|
||||
FilePath = popup.SavePath;
|
||||
ChosenFormat = popup.SaveFormat;
|
||||
ExportOutput = popup.ExportOutput;
|
||||
|
||||
|
||||
ExportConfig.ExportSize = new VecI(FileWidth, FileHeight);
|
||||
ExportConfig.ExportOutput = ExportOutput.Name;
|
||||
ExportConfig.ExportFramesToFolder = popup.FolderExport;
|
||||
ExportConfig.AnimationRenderer = ChosenFormat is VideoFileType ? new FFMpegRenderer()
|
||||
{
|
||||
Size = new VecI(FileWidth, FileHeight),
|
||||
OutputFormat = ChosenFormat.PrimaryExtension.Replace(".", ""),
|
||||
FrameRate = document.AnimationDataViewModel.FrameRateBindable
|
||||
FrameRate = document.AnimationDataViewModel.FrameRateBindable,
|
||||
QualityPreset = (QualityPreset)popup.AnimationPresetIndex
|
||||
}
|
||||
: null;
|
||||
ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;
|
||||
|
@ -7,6 +7,7 @@
|
||||
xmlns:indicators="clr-namespace:PixiEditor.Views.Indicators"
|
||||
xmlns:input1="clr-namespace:PixiEditor.Views.Input"
|
||||
xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
|
||||
xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
|
||||
CanResize="False"
|
||||
CanMinimize="False"
|
||||
SizeToContent="WidthAndHeight"
|
||||
@ -27,7 +28,26 @@
|
||||
</TabControl.Styles>
|
||||
<TabControl.Items>
|
||||
<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">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@ -13,6 +13,7 @@ using PixiEditor.Helpers;
|
||||
using PixiEditor.Models.Files;
|
||||
using PixiEditor.Models.IO;
|
||||
using Drawie.Numerics;
|
||||
using PixiEditor.AnimationRenderer.Core;
|
||||
using PixiEditor.UI.Common.Localization;
|
||||
using PixiEditor.ViewModels.Document;
|
||||
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>(
|
||||
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
|
||||
{
|
||||
get => GetValue(SizeHintProperty);
|
||||
@ -171,6 +180,14 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
||||
|
||||
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 Image[]? videoPreviewFrames = [];
|
||||
private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
|
||||
@ -179,6 +196,9 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
||||
|
||||
private Task? generateSpriteSheetTask;
|
||||
|
||||
public static readonly StyledProperty<int> AnimationPresetIndexProperty
|
||||
= AvaloniaProperty.Register<ExportFilePopup, int>("AnimationPresetIndex", 4);
|
||||
|
||||
static ExportFilePopup()
|
||||
{
|
||||
SaveWidthProperty.Changed.Subscribe(RerenderPreview);
|
||||
@ -193,8 +213,10 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
||||
{
|
||||
SaveWidth = imageWidth;
|
||||
SaveHeight = imageHeight;
|
||||
QualityPresetValues = Enum.GetValues(typeof(QualityPreset));
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = this;
|
||||
Loaded += (_, _) => sizePicker.FocusWidthPicker();
|
||||
|
||||
@ -467,37 +489,64 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
string fileName = SupportedFilesHelper.FixFileExtension(file.Path.LocalPath, SaveFormat);
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user