Browse Source

Merge branch 'anim-render-pipe' into avalonia-rewrite

flabbet 1 year ago
parent
commit
59fee76a1c

+ 4 - 2
src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs

@@ -1,6 +1,8 @@
-namespace PixiEditor.AnimationRenderer.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+
+namespace PixiEditor.AnimationRenderer.Core;
 
 
 public interface IAnimationRenderer
 public interface IAnimationRenderer
 {
 {
-    public Task<bool> RenderAsync(string framesPath, string outputPath);
+    public Task<bool> RenderAsync(List<Image> imageStream, string outputPath);
 }
 }

+ 4 - 0
src/PixiEditor.AnimationRenderer.Core/PixiEditor.AnimationRenderer.Core.csproj

@@ -6,4 +6,8 @@
         <Nullable>enable</Nullable>
         <Nullable>enable</Nullable>
     </PropertyGroup>
     </PropertyGroup>
 
 
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj" />
+    </ItemGroup>
+
 </Project>
 </Project>

+ 51 - 29
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -3,7 +3,9 @@ using System.Reflection;
 using FFMpegCore;
 using FFMpegCore;
 using FFMpegCore.Arguments;
 using FFMpegCore.Arguments;
 using FFMpegCore.Enums;
 using FFMpegCore.Enums;
+using FFMpegCore.Pipes;
 using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.AnimationRenderer.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AnimationRenderer.FFmpeg;
 namespace PixiEditor.AnimationRenderer.FFmpeg;
@@ -14,24 +16,8 @@ public class FFMpegRenderer : IAnimationRenderer
     public string OutputFormat { get; set; } = "mp4";
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
     public VecI Size { get; set; }
 
 
-    public async Task<bool> RenderAsync(string framesPath, string outputPath)
+    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath)
     {
     {
-        string[] frames = Directory.GetFiles(framesPath, "*.png");
-        if (frames.Length == 0)
-        {
-            return false;
-        }
-
-        string[] finalFrames = new string[frames.Length];
-
-        for (int i = 0; i < frames.Length; i++)
-        {
-            if (int.TryParse(Path.GetFileNameWithoutExtension(frames[i]), out int frameNumber))
-            {
-                finalFrames[frameNumber - 1] = frames[i];
-            }
-        }
-        
         string path = "ThirdParty/{0}/ffmpeg/bin";
         string path = "ThirdParty/{0}/ffmpeg/bin";
 #if WINDOWS
 #if WINDOWS
         path = string.Format(path, "Windows");
         path = string.Format(path, "Windows");
@@ -48,19 +34,47 @@ public class FFMpegRenderer : IAnimationRenderer
 
 
         try
         try
         {
         {
+            List<ImgFrame> frames = new();
+            
+            foreach (var frame in rawFrames)
+            {
+                frames.Add(new ImgFrame(frame));
+            }
+
+            RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
+            
+            string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
+            
+            if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
+            {
+                Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
+            }
+            
             if (RequiresPaletteGeneration())
             if (RequiresPaletteGeneration())
             {
             {
-                GeneratePalette(finalFrames, framesPath);
+                GeneratePalette(streamPipeSource, paletteTempPath);
             }
             }
+            
+            streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
 
             var args = FFMpegArguments
             var args = FFMpegArguments
-                .FromConcatInput(finalFrames, options =>
+                .FromPipeInput(streamPipeSource, options =>
                 {
                 {
                     options.WithFramerate(FrameRate);
                     options.WithFramerate(FrameRate);
                 });
                 });
 
 
-            var outputArgs = GetProcessorForFormat(args, framesPath, outputPath);
-            return await outputArgs.ProcessAsynchronously();
+            var outputArgs = GetProcessorForFormat(args, outputPath, paletteTempPath);
+            var result = await outputArgs.ProcessAsynchronously();
+            
+            if (RequiresPaletteGeneration())
+            {
+                File.Delete(paletteTempPath);
+                Directory.Delete(Path.GetDirectoryName(paletteTempPath));
+            }
+            
+            DisposeStream(frames);
+            
+            return result;
         }
         }
         catch (Exception e)
         catch (Exception e)
         {
         {
@@ -69,20 +83,29 @@ public class FFMpegRenderer : IAnimationRenderer
         }
         }
     }
     }
 
 
-    private FFMpegArgumentProcessor GetProcessorForFormat(FFMpegArguments args, string framesPath, string outputPath)
+    private void DisposeStream(List<ImgFrame> frames)
+    {
+        foreach (var frame in frames)
+        {
+            frame.Dispose();
+        }
+    }
+
+    private FFMpegArgumentProcessor GetProcessorForFormat(FFMpegArguments args, string outputPath,
+        string paletteTempPath)
     {
     {
         return OutputFormat switch
         return OutputFormat switch
         {
         {
-            "gif" => GetGifArguments(args, framesPath, outputPath),
+            "gif" => GetGifArguments(args, outputPath, paletteTempPath),
             "mp4" => GetMp4Arguments(args, outputPath),
             "mp4" => GetMp4Arguments(args, outputPath),
             _ => throw new NotSupportedException($"Output format {OutputFormat} is not supported")
             _ => throw new NotSupportedException($"Output format {OutputFormat} is not supported")
         };
         };
     }
     }
 
 
-    private FFMpegArgumentProcessor GetGifArguments(FFMpegArguments args, string framesPath, string outputPath)
+    private FFMpegArgumentProcessor GetGifArguments(FFMpegArguments args, string outputPath, string paletteTempPath)
     {
     {
         return args
         return args
-            .AddFileInput(Path.Combine(framesPath, "palette.png"))
+            .AddFileInput(paletteTempPath)
             .OutputToFile(outputPath, true, options =>
             .OutputToFile(outputPath, true, options =>
             {
             {
                 options.WithCustomArgument(
                 options.WithCustomArgument(
@@ -108,15 +131,14 @@ public class FFMpegRenderer : IAnimationRenderer
         return OutputFormat == "gif";
         return OutputFormat == "gif";
     }
     }
 
 
-    private void GeneratePalette(string[] frames, string path)
+    private void GeneratePalette(IPipeSource imageStream, string path)
     {
     {
-        string palettePath = Path.Combine(path, "palette.png");
         FFMpegArguments
         FFMpegArguments
-            .FromConcatInput(frames, options =>
+            .FromPipeInput(imageStream, options =>
             {
             {
                 options.WithFramerate(FrameRate);
                 options.WithFramerate(FrameRate);
             })
             })
-            .OutputToFile(palettePath, true, options =>
+            .OutputToFile(path, true, options =>
             {
             {
                 options
                 options
                     .WithCustomArgument($"-vf \"palettegen\"");
                     .WithCustomArgument($"-vf \"palettegen\"");

+ 55 - 0
src/PixiEditor.AnimationRenderer.FFmpeg/ImgFrame.cs

@@ -0,0 +1,55 @@
+using FFMpegCore.Pipes;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+
+namespace PixiEditor.AnimationRenderer.FFmpeg;
+
+public class ImgFrame : IVideoFrame, IDisposable
+{
+    public Image Image { get; }
+
+    public int Width => Image.Width;
+    public int Height => Image.Height;
+    public string Format => ToStreamFormat(); 
+
+    private Bitmap encoded;
+    
+    public ImgFrame(Image image)
+    {
+        Image = image;
+        encoded = Bitmap.FromImage(image);
+    }
+
+    public void Serialize(Stream pipe)
+    {
+        var bytes = encoded.Bytes;
+        pipe.Write(bytes, 0, bytes.Length);
+    }
+
+    public async Task SerializeAsync(Stream pipe, CancellationToken token)
+    {
+        await pipe.WriteAsync(encoded.Bytes, 0, encoded.Bytes.Length, token).ConfigureAwait(false); 
+    }
+    
+    private string ToStreamFormat()
+    {
+        switch (encoded.Info.ColorType)
+        {
+            case ColorType.Gray8:
+                return "gray8";
+            case ColorType.Bgra8888:
+                return "bgra";
+            case ColorType.Rgba8888:
+                return "rgba";
+            case ColorType.Rgb565:
+                return "rgb565";
+            default:
+                throw new NotSupportedException($"Color type {Image.Info.ColorType} is not supported.");
+        } 
+    }
+
+    public void Dispose()
+    {
+        encoded.Dispose();
+    }
+}

+ 16 - 4
src/PixiEditor.AvaloniaUI/Models/Files/VideoFileType.cs

@@ -1,17 +1,23 @@
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 
 
 namespace PixiEditor.AvaloniaUI.Models.Files;
 namespace PixiEditor.AvaloniaUI.Models.Files;
 
 
 internal abstract class VideoFileType : IoFileType
 internal abstract class VideoFileType : IoFileType
 {
 {
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Video;
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Video;
-    public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config)
+
+    public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
+        ExportConfig config)
     {
     {
         if (config.AnimationRenderer is null)
         if (config.AnimationRenderer is null)
             return SaveResult.UnknownError;
             return SaveResult.UnknownError;
-        
-        document.RenderFrames(Paths.TempRenderingPath, surface =>
+
+        List<Image> frames = new(); 
+
+        document.RenderFrames(frames, surface =>
         {
         {
             if (config.ExportSize != surface.Size)
             if (config.ExportSize != surface.Size)
             {
             {
@@ -20,8 +26,14 @@ internal abstract class VideoFileType : IoFileType
 
 
             return surface;
             return surface;
         });
         });
+
+        var result = await config.AnimationRenderer.RenderAsync(frames, pathWithExtension);
         
         
-        var result = await config.AnimationRenderer.RenderAsync(Paths.TempRenderingPath, pathWithExtension);
+        foreach (var frame in frames)
+        {
+            frame.Dispose();
+        } 
+
         return result ? SaveResult.Success : SaveResult.UnknownError;
         return result ? SaveResult.Success : SaveResult.UnknownError;
     }
     }
 }
 }

+ 4 - 13
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -887,20 +887,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
         }
     }
     }
 
 
-    public bool RenderFrames(string tempRenderingPath, Func<Surface, Surface> processFrameAction = null)
+    public bool RenderFrames(List<Image> frames, Func<Surface, Surface> processFrameAction = null)
     {
     {
         if (AnimationDataViewModel.KeyFrames.Count == 0)
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return false;
             return false;
 
 
-        if (!Directory.Exists(tempRenderingPath))
-        {
-            Directory.CreateDirectory(tempRenderingPath);
-        }
-        else
-        {
-            ClearTempFolder(tempRenderingPath);
-        }
-
         var keyFrames = AnimationDataViewModel.KeyFrames;
         var keyFrames = AnimationDataViewModel.KeyFrames;
         var firstFrame = keyFrames.Min(x => x.StartFrameBindable);
         var firstFrame = keyFrames.Min(x => x.StartFrameBindable);
         var lastFrame = keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
         var lastFrame = keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
@@ -919,9 +910,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 surface = processFrameAction(surface.AsT1);
                 surface = processFrameAction(surface.AsT1);
             }
             }
 
 
-            using var stream = new FileStream(Path.Combine(tempRenderingPath, $"{i}.png"), FileMode.Create);
-            surface.AsT1.DrawingSurface.Snapshot().Encode().SaveTo(stream);
-            stream.Position = 0;
+
+            var snapshot = surface.AsT1.DrawingSurface.Snapshot();
+            frames.Add(snapshot);
         }
         }
 
 
         return true;
         return true;

+ 3 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IBitmapImplementation.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
@@ -11,4 +12,6 @@ public interface IBitmapImplementation
     public object GetNativeBitmap(IntPtr objectPointer);
     public object GetNativeBitmap(IntPtr objectPointer);
     public Bitmap FromImage(IntPtr snapshot);
     public Bitmap FromImage(IntPtr snapshot);
     public VecI GetSize(IntPtr objectPointer);
     public VecI GetSize(IntPtr objectPointer);
+    public byte[] GetBytes(IntPtr objectPointer);
+    public ImageInfo GetInfo(IntPtr objectPointer);
 }
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Bridge/Operations/IImageImplementation.cs

@@ -23,5 +23,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
         public object GetNativeImage(IntPtr objectPointer);
         public object GetNativeImage(IntPtr objectPointer);
         public Image Clone(Image image);
         public Image Clone(Image image);
         public Pixmap PeekPixels(IntPtr objectPointer);
         public Pixmap PeekPixels(IntPtr objectPointer);
+        public ImageInfo GetImageInfo(IntPtr objectPointer);
     }
     }
 }
 }

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/Bitmap.cs

@@ -16,6 +16,8 @@ public class Bitmap : NativeObject
     }
     }
 
 
     public override object Native => DrawingBackendApi.Current.BitmapImplementation.GetNativeBitmap(ObjectPointer);
     public override object Native => DrawingBackendApi.Current.BitmapImplementation.GetNativeBitmap(ObjectPointer);
+    public byte[] Bytes => DrawingBackendApi.Current.BitmapImplementation.GetBytes(ObjectPointer);
+    public ImageInfo Info => DrawingBackendApi.Current.BitmapImplementation.GetInfo(ObjectPointer);
 
 
     public override void Dispose()
     public override void Dispose()
     {
     {

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/ImageData/Image.cs

@@ -20,6 +20,8 @@ namespace PixiEditor.DrawingApi.Core.Surfaces.ImageData
         
         
         public int Height => DrawingBackendApi.Current.ImageImplementation.GetHeight(ObjectPointer);
         public int Height => DrawingBackendApi.Current.ImageImplementation.GetHeight(ObjectPointer);
         
         
+        public ImageInfo Info => DrawingBackendApi.Current.ImageImplementation.GetImageInfo(ObjectPointer);
+        
         public VecI Size => new VecI(Width, Height);
         public VecI Size => new VecI(Width, Height);
         
         
         public Image(IntPtr objPtr) : base(objPtr)
         public Image(IntPtr objPtr) : base(objPtr)

+ 13 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -43,6 +44,18 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return new VecI(bitmap.Width, bitmap.Height);
             return new VecI(bitmap.Width, bitmap.Height);
         }
         }
 
 
+        public byte[] GetBytes(IntPtr objectPointer)
+        {
+            SKBitmap bitmap = ManagedInstances[objectPointer];
+            return bitmap.Bytes; 
+        }
+        
+        public ImageInfo GetInfo(IntPtr objectPointer)
+        {
+            SKBitmap bitmap = ManagedInstances[objectPointer];
+            return bitmap.Info.ToImageInfo();
+        }
+
         public object GetNativeBitmap(IntPtr objectPointer)
         public object GetNativeBitmap(IntPtr objectPointer)
         {
         {
             return ManagedInstances[objectPointer];
             return ManagedInstances[objectPointer];

+ 6 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageImplementation.cs

@@ -137,6 +137,12 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return _pixmapImplementation.CreateFrom(nativePixmap);
             return _pixmapImplementation.CreateFrom(nativePixmap);
         }
         }
 
 
+        public ImageInfo GetImageInfo(IntPtr objectPointer)
+        {
+            var info = ManagedInstances[objectPointer].Info;
+            return info.ToImageInfo();
+        }
+
         public object GetNativeImage(IntPtr objectPointer)
         public object GetNativeImage(IntPtr objectPointer)
         {
         {
             return ManagedInstances[objectPointer];
             return ManagedInstances[objectPointer];