Browse Source

Add proper Stream-based generation support

Marcin Ziąbek 6 months ago
parent
commit
b0d45b0a62

+ 15 - 5
Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
+using System.IO;
 using System.Text;
 using System.Text;
 using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
@@ -13,7 +14,8 @@ namespace QuestPDF.Drawing.DocumentCanvases
         private SkCanvas? CurrentPageCanvas { get; set; }
         private SkCanvas? CurrentPageCanvas { get; set; }
         private ProxyDrawingCanvas DrawingCanvas { get; } = new();
         private ProxyDrawingCanvas DrawingCanvas { get; } = new();
         
         
-        private SkWriteStream WriteStream { get; set; }
+        private MemoryStream WriteStream { get; set; }
+        private SkWriteStream SkiaStream { get; set; }
         internal ICollection<string> Images { get; } = new List<string>();
         internal ICollection<string> Images { get; } = new List<string>();
         
         
         #region IDisposable
         #region IDisposable
@@ -28,6 +30,7 @@ namespace QuestPDF.Drawing.DocumentCanvases
         {
         {
             CurrentPageCanvas?.Dispose();
             CurrentPageCanvas?.Dispose();
             WriteStream?.Dispose();
             WriteStream?.Dispose();
+            SkiaStream?.Dispose();
             DrawingCanvas?.Dispose();
             DrawingCanvas?.Dispose();
             
             
             GC.SuppressFinalize(this);
             GC.SuppressFinalize(this);
@@ -46,13 +49,17 @@ namespace QuestPDF.Drawing.DocumentCanvases
         {
         {
             CurrentPageCanvas?.Dispose();
             CurrentPageCanvas?.Dispose();
             WriteStream?.Dispose();
             WriteStream?.Dispose();
+            SkiaStream?.Dispose();
         }
         }
 
 
         public void BeginPage(Size size)
         public void BeginPage(Size size)
         {
         {
             WriteStream?.Dispose();
             WriteStream?.Dispose();
-            WriteStream = new SkWriteStream();
-            CurrentPageCanvas = SkSvgCanvas.CreateSvg(size.Width, size.Height, WriteStream);
+            SkiaStream?.Dispose();
+            
+            WriteStream = new MemoryStream();
+            SkiaStream = new SkWriteStream(WriteStream);
+            CurrentPageCanvas = SkSvgCanvas.CreateSvg(size.Width, size.Height, SkiaStream);
             
             
             DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
             DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
             DrawingCanvas.SetZIndex(0);
             DrawingCanvas.SetZIndex(0);
@@ -69,10 +76,13 @@ namespace QuestPDF.Drawing.DocumentCanvases
             CurrentPageCanvas.Dispose();
             CurrentPageCanvas.Dispose();
             CurrentPageCanvas = null;
             CurrentPageCanvas = null;
             
             
-            using var data = WriteStream.DetachData();
-            var svgImage = Encoding.UTF8.GetString(data.ToBytes());
+            SkiaStream.Flush();
+
+            var data = WriteStream.ToArray();
+            var svgImage = Encoding.UTF8.GetString(data);
             Images.Add(svgImage);
             Images.Add(svgImage);
             
             
+            SkiaStream.Dispose();
             WriteStream.Dispose();
             WriteStream.Dispose();
         }
         }
         
         

+ 19 - 23
Source/QuestPDF/Fluent/GenerateExtensions.cs

@@ -17,11 +17,9 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static byte[] GeneratePdf(this IDocument document)
         public static byte[] GeneratePdf(this IDocument document)
         {
         {
-            using var stream = new SkWriteStream();
-            DocumentGenerator.GeneratePdf(stream, document);
-            
-            using var data = stream.DetachData();
-            return data.ToBytes();
+            using var memoryStream = new MemoryStream();
+            document.GeneratePdf(memoryStream);
+            return memoryStream.ToArray();
         }
         }
         
         
         /// <summary>
         /// <summary>
@@ -29,12 +27,11 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static void GeneratePdf(this IDocument document, string filePath)
         public static void GeneratePdf(this IDocument document, string filePath)
         {
         {
-            var data = document.GeneratePdf();
-            
             if (File.Exists(filePath))
             if (File.Exists(filePath))
                 File.Delete(filePath);
                 File.Delete(filePath);
             
             
-            File.WriteAllBytes(filePath, data);
+            using var fileStream = File.Create(filePath);
+            document.GeneratePdf(fileStream);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -42,8 +39,9 @@ namespace QuestPDF.Fluent
         /// </summary>
         /// </summary>
         public static void GeneratePdf(this IDocument document, Stream stream)
         public static void GeneratePdf(this IDocument document, Stream stream)
         {
         {
-            var data = document.GeneratePdf();
-            stream.Write(data, 0, data.Length);
+            using var skiaStream = new SkWriteStream(stream);
+            DocumentGenerator.GeneratePdf(skiaStream, document);
+            skiaStream.Flush();
         }
         }
         
         
         private static int GenerateAndShowCounter = 0;
         private static int GenerateAndShowCounter = 0;
@@ -72,14 +70,9 @@ namespace QuestPDF.Fluent
         /// </remarks>
         /// </remarks>
         public static byte[] GenerateXps(this IDocument document)
         public static byte[] GenerateXps(this IDocument document)
         {
         {
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-                throw new PlatformNotSupportedException("XPS generation is only supported on the Windows platform.");
-            
-            using var stream = new SkWriteStream();
-            DocumentGenerator.GenerateXps(stream, document);
-            
-            using var data = stream.DetachData();
-            return data.ToBytes();
+            using var memoryStream = new MemoryStream();
+            document.GenerateXps(memoryStream);
+            return memoryStream.ToArray();
         }
         }
         
         
         /// <summary>
         /// <summary>
@@ -90,12 +83,11 @@ namespace QuestPDF.Fluent
         /// </remarks>
         /// </remarks>
         public static void GenerateXps(this IDocument document, string filePath)
         public static void GenerateXps(this IDocument document, string filePath)
         {
         {
-            var data = document.GenerateXps();
-            
             if (File.Exists(filePath))
             if (File.Exists(filePath))
                 File.Delete(filePath);
                 File.Delete(filePath);
             
             
-            File.WriteAllBytes(filePath, data);
+            using var fileStream = File.Create(filePath);
+            document.GenerateXps(fileStream);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -106,8 +98,12 @@ namespace QuestPDF.Fluent
         /// </remarks>
         /// </remarks>
         public static void GenerateXps(this IDocument document, Stream stream)
         public static void GenerateXps(this IDocument document, Stream stream)
         {
         {
-            var data = document.GenerateXps();
-            stream.Write(data, 0, data.Length);
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                throw new PlatformNotSupportedException("XPS generation is only supported on the Windows platform.");
+            
+            using var skiaStream = new SkWriteStream(stream);
+            DocumentGenerator.GenerateXps(skiaStream, document);
+            skiaStream.Flush();
         }
         }
         
         
         /// <summary>
         /// <summary>

BIN
Source/QuestPDF/Runtimes/linux-arm64/native/libQuestPdfSkia.so


BIN
Source/QuestPDF/Runtimes/linux-musl-x64/native/libQuestPdfSkia.so


BIN
Source/QuestPDF/Runtimes/linux-x64/native/libQuestPdfSkia.so


BIN
Source/QuestPDF/Runtimes/osx-arm64/native/libQuestPdfSkia.dylib


BIN
Source/QuestPDF/Runtimes/osx-x64/native/libQuestPdfSkia.dylib


BIN
Source/QuestPDF/Runtimes/win-x64/native/QuestPdfSkia.dll


BIN
Source/QuestPDF/Runtimes/win-x86/native/QuestPdfSkia.dll


+ 1 - 1
Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs

@@ -7,7 +7,7 @@ namespace QuestPDF.Skia;
 
 
 internal static class SkNativeDependencyCompatibilityChecker
 internal static class SkNativeDependencyCompatibilityChecker
 {
 {
-    private const int ExpectedNativeLibraryVersion = 3;
+    private const int ExpectedNativeLibraryVersion = 4;
     
     
     private static NativeDependencyCompatibilityChecker Instance { get; } = new()
     private static NativeDependencyCompatibilityChecker Instance { get; } = new()
     {
     {

+ 32 - 10
Source/QuestPDF/Skia/SkWriteStream.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.IO;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 
 
 namespace QuestPDF.Skia;
 namespace QuestPDF.Skia;
@@ -6,17 +7,34 @@ namespace QuestPDF.Skia;
 internal sealed class SkWriteStream : IDisposable
 internal sealed class SkWriteStream : IDisposable
 {
 {
     public IntPtr Instance { get; private set; }
     public IntPtr Instance { get; private set; }
-    
-    public SkWriteStream()
+    private GCHandle CallbackHandle { get; }
+
+    public SkWriteStream(Stream stream)
     {
     {
-        Instance = API.write_stream_create();
+        var nativeCallback = new API.ByteArrayCallback((data, size) =>
+        {
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+            unsafe
+            {
+                var span = new ReadOnlySpan<byte>((void*)data, size);
+                stream.Write(span);
+            }
+#else
+            var managedArray = new byte[size];
+            Marshal.Copy(data, managedArray, 0, size);
+            stream?.Write(managedArray, 0, managedArray.Length);
+#endif
+        });
+
+        CallbackHandle = GCHandle.Alloc(nativeCallback);
+        
+        Instance = API.write_stream_create(nativeCallback);
         SkiaAPI.EnsureNotNull(Instance);
         SkiaAPI.EnsureNotNull(Instance);
     }
     }
     
     
-    public SkData DetachData()
+    public void Flush()
     {
     {
-        var dataInstance = API.write_stream_detach_data(Instance);
-        return new SkData(dataInstance);
+        API.write_stream_flush(Instance);
     }
     }
     
     
     ~SkWriteStream()
     ~SkWriteStream()
@@ -29,7 +47,8 @@ internal sealed class SkWriteStream : IDisposable
     {
     {
         if (Instance == IntPtr.Zero)
         if (Instance == IntPtr.Zero)
             return;
             return;
-        
+     
+        CallbackHandle.Free();
         API.write_stream_delete(Instance);
         API.write_stream_delete(Instance);
         Instance = IntPtr.Zero;
         Instance = IntPtr.Zero;
         GC.SuppressFinalize(this);
         GC.SuppressFinalize(this);
@@ -37,13 +56,16 @@ internal sealed class SkWriteStream : IDisposable
     
     
     private static class API
     private static class API
     {
     {
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+        public delegate void ByteArrayCallback(IntPtr data, int size);
+
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern IntPtr write_stream_create();
+        public static extern IntPtr write_stream_create(ByteArrayCallback callback);
     
     
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern void write_stream_delete(IntPtr stream);
+        public static extern void write_stream_flush(IntPtr stream);
         
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern IntPtr write_stream_detach_data(IntPtr stream);    
+        public static extern void write_stream_delete(IntPtr stream);
     }
     }
 }
 }