Browse Source

Prototype: previewer communication using sockets

Marcin Ziąbek 1 year ago
parent
commit
f9a7ffdc69

+ 33 - 3
Source/QuestPDF.Examples/ImageExamples.cs

@@ -189,7 +189,7 @@ namespace QuestPDF.Examples
                             .FontSize(192)
                             .FontSize(192)
                             .FontColor(Colors.Blue.Medium)
                             .FontColor(Colors.Blue.Medium)
                             .Bold();
                             .Bold();
-                        
+
                         var image = LoadImageWithTransparency("photo.jpg", 0.75f);
                         var image = LoadImageWithTransparency("photo.jpg", 0.75f);
                         page.Foreground().Image(image);
                         page.Foreground().Image(image);
                     });
                     });
@@ -200,18 +200,48 @@ namespace QuestPDF.Examples
                 using var originalImage = SKImage.FromEncodedData(fileName);
                 using var originalImage = SKImage.FromEncodedData(fileName);
 
 
                 using var surface = SKSurface.Create(originalImage.Width, originalImage.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
                 using var surface = SKSurface.Create(originalImage.Width, originalImage.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
-                using var canvas = surface.Canvas;                
+                using var canvas = surface.Canvas;
 
 
                 using var transparencyPaint = new SKPaint
                 using var transparencyPaint = new SKPaint
                 {
                 {
                     ColorFilter = SKColorFilter.CreateBlendMode(SKColors.White.WithAlpha((byte)(transparency * 255)), SKBlendMode.DstIn)
                     ColorFilter = SKColorFilter.CreateBlendMode(SKColors.White.WithAlpha((byte)(transparency * 255)), SKBlendMode.DstIn)
                 };
                 };
-                
+
                 canvas.DrawImage(originalImage, new SKPoint(0, 0), transparencyPaint);
                 canvas.DrawImage(originalImage, new SKPoint(0, 0), transparencyPaint);
 
 
                 var encodedImage = surface.Snapshot().Encode(SKEncodedImageFormat.Png, 100).ToArray();
                 var encodedImage = surface.Snapshot().Encode(SKEncodedImageFormat.Png, 100).ToArray();
                 return Image.FromBinaryData(encodedImage);
                 return Image.FromBinaryData(encodedImage);
             }
             }
         }
         }
+
+        [Test]
+        public void ImageFilesShouldBeDisposableImmediatelyAfterDocumentGeneration()
+        {
+            var files = Enumerable.Range(0, 100).Select(x => $"temp_{x}.jpg").ToList();
+
+            foreach (var file in files)
+                File.Copy("photo.jpg", file);
+
+            var document = Document.Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(24);
+
+                    page.Content().Column(column =>
+                    {
+                        column.Spacing(24);
+
+                        foreach (var file in files)
+                            column.Item().Image(file).UseOriginalImage();
+                    });
+                });
+            });
+
+            document.GeneratePdfAndShow();
+
+            foreach (var file in files)
+                File.Delete(file);
+        }
     }
     }
 }
 }

+ 60 - 62
Source/QuestPDF.Previewer/CommunicationService.cs

@@ -1,9 +1,4 @@
 using System.Text.Json;
 using System.Text.Json;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace QuestPDF.Previewer;
 namespace QuestPDF.Previewer;
@@ -13,10 +8,9 @@ class CommunicationService
     public static CommunicationService Instance { get; } = new ();
     public static CommunicationService Instance { get; } = new ();
     
     
     public event Action<DocumentStructure>? OnDocumentUpdated;
     public event Action<DocumentStructure>? OnDocumentUpdated;
-    public Func<ICollection<PageSnapshotIndex>>? OnPageSnapshotsRequested { get; set; }
-    public Action<ICollection<RenderedPageSnapshot>> OnPageSnapshotsProvided  { get; set; }
+    public Action<RenderedPageSnapshot> OnPageSnapshotsProvided  { get; set; }
 
 
-    private WebApplication? Application { get; set; }
+    private SocketServer? Server { get; set; }
 
 
     private readonly JsonSerializerOptions JsonSerializerOptions = new()
     private readonly JsonSerializerOptions JsonSerializerOptions = new()
     {
     {
@@ -28,69 +22,73 @@ class CommunicationService
         
         
     }
     }
     
     
-    public Task Start(int port)
+    public void Start(int port)
     {
     {
-        var builder = WebApplication.CreateBuilder();
-        builder.Services.AddLogging(x => x.ClearProviders());
-        builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
-        Application = builder.Build();
-
-        Application.MapGet("ping", HandlePing);
-        Application.MapGet("version", HandleVersion);
-        Application.MapPost("preview/update", HandlePreviewRefresh);
-        Application.MapGet("preview/getRenderingRequests", HandleGetRequests);
-        Application.MapPost("preview/provideRenderedImages", HandleProvidedSnapshotImages);
+        Server = new SocketServer("127.0.0.1", port);
+        
+        Server.OnMessageReceived += async message =>
+        {
+            var content = JsonDocument.Parse(message).RootElement;
+            var channel = content.GetProperty("Channel").GetString();
             
             
-        return Application.RunAsync($"http://localhost:{port}/");
-    }
-
-    public async Task Stop()
-    {
-        await Application.StopAsync();
-        await Application.DisposeAsync();
-    }
+            if (channel == "ping/check")
+            {
+                if (OnDocumentUpdated == null)
+                    return;
+                
+                var response = new SocketMessage<string>
+                {
+                    Channel = "ping/ok",
+                    Payload = GetType().Assembly.GetName().Version.ToString()
+                };
+                
+                Server.SendMessage(JsonSerializer.Serialize(response));
+            }
+            else if (channel == "version/check")
+            {
+                var response = new SocketMessage<string>
+                {
+                    Channel = "version/provide",
+                    Payload = GetType().Assembly.GetName().Version.ToString()
+                };
+                
+                Server.SendMessage(JsonSerializer.Serialize(response));
+            }
+            else if (channel == "preview/update")
+            {
+                var documentStructure = content.GetProperty("Payload").Deserialize<DocumentStructure>();
+                Task.Run(() => OnDocumentUpdated(documentStructure));
+            }
+            else if (channel == "preview/updatePage")
+            {
+                var previewData = content.GetProperty("Payload").Deserialize<PageSnapshotCommunicationData>();
+                var image = SKImage.FromEncodedData(previewData.ImageData).ToRasterImage(true);
 
 
-    private async Task<IResult> HandlePing()
-    {
-        return OnDocumentUpdated == null 
-            ? Results.StatusCode(StatusCodes.Status503ServiceUnavailable) 
-            : Results.Ok();
-    }
-    
-    private async Task<IResult> HandleVersion()
-    {
-        return Results.Json(GetType().Assembly.GetName().Version);
-    }
-    
-    private async Task HandlePreviewRefresh(DocumentStructure documentStructure)
-    {
-        Task.Run(() => OnDocumentUpdated(documentStructure));
+                var renderedPage = new RenderedPageSnapshot
+                {
+                    ZoomLevel = previewData.ZoomLevel, 
+                    PageIndex = previewData.PageIndex, 
+                    Image = image
+                };
+            
+                Task.Run(() => OnPageSnapshotsProvided(renderedPage));
+            }
+        };
+        
+        Server.Start();
     }
     }
 
 
-    private async Task<ICollection<PageSnapshotIndex>> HandleGetRequests()
+    public void RequestNewPage(PageSnapshotIndex index)
     {
     {
-        return OnPageSnapshotsRequested();
-    }
-    
-    private async Task HandleProvidedSnapshotImages(HttpRequest request)
-    {
-        var renderedPageIndexes = JsonSerializer.Deserialize<ICollection<PageSnapshotIndex>>(request.Form["metadata"], JsonSerializerOptions);
-        var renderedPages = new List<RenderedPageSnapshot>();
-
-        foreach (var index in renderedPageIndexes)
+        Task.Run(() =>
         {
         {
-            using var memoryStream = new MemoryStream();
-            await request.Form.Files.GetFile(index.ToString()).CopyToAsync(memoryStream);
-            var image = SKImage.FromEncodedData(memoryStream.ToArray()).ToRasterImage(true);
-
-            var renderedPage = new RenderedPageSnapshot
+            var message = new SocketMessage<PageSnapshotIndex>
             {
             {
-                ZoomLevel = index.ZoomLevel, PageIndex = index.PageIndex, Image = image
+                Channel = "preview/requestPage",
+                Payload = index
             };
             };
             
             
-            renderedPages.Add(renderedPage);
-        }
-
-        Task.Run(() => OnPageSnapshotsProvided(renderedPages));
+            Server.SendMessage(JsonSerializer.Serialize(message));
+        });
     }
     }
 }
 }

+ 2 - 3
Source/QuestPDF.Previewer/InteractiveCanvas.cs

@@ -91,10 +91,9 @@ class InteractiveCanvas : ICustomDrawOperation
             .ToList();
             .ToList();
     }
     }
 
 
-    public void AddSnapshots(ICollection<RenderedPageSnapshot> snapshots)
+    public void AddSnapshots(RenderedPageSnapshot snapshot)
     {
     {
-        foreach (var snapshot in snapshots)
-            PageSnapshotCache.Add(snapshot);
+        PageSnapshotCache.Add(snapshot);
     }
     }
     
     
     #endregion
     #endregion

+ 7 - 0
Source/QuestPDF.Previewer/Models.cs

@@ -22,6 +22,13 @@ class PageSnapshotIndex
     public override string ToString() => $"{ZoomLevel}/{PageIndex}";
     public override string ToString() => $"{ZoomLevel}/{PageIndex}";
 }
 }
 
 
+class PageSnapshotCommunicationData
+{
+    public int PageIndex { get; set; }
+    public int ZoomLevel { get; set; }
+    public byte[] ImageData { get; set; }
+}
+
 class RenderedPageSnapshot : PageSnapshotIndex
 class RenderedPageSnapshot : PageSnapshotIndex
 {
 {
     public SKImage Image { get; set; }
     public SKImage Image { get; set; }

+ 29 - 3
Source/QuestPDF.Previewer/PreviewerControl.cs

@@ -29,17 +29,43 @@ namespace QuestPDF.Previewer
         
         
         public PreviewerControl()
         public PreviewerControl()
         {
         {
+            var requestedRenderings = new HashSet<(int pageIndex, int zoomLevel)>();
+            
             CommunicationService.Instance.OnDocumentUpdated += document =>
             CommunicationService.Instance.OnDocumentUpdated += document =>
             {
             {
+                requestedRenderings.Clear();
+                
                 InteractiveCanvas.SetNewDocumentStructure(document);
                 InteractiveCanvas.SetNewDocumentStructure(document);
                 Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
                 Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
             };
             };
             
             
-            CommunicationService.Instance.OnPageSnapshotsRequested += InteractiveCanvas.GetMissingSnapshots;
+            Task.Run(async () =>
+            {
+                while (true)
+                {
+                    var missingSnapshots = InteractiveCanvas.GetMissingSnapshots().Select(x => (x.PageIndex, x.ZoomLevel)).ToList();
+                    
+                    if (!missingSnapshots.Any())
+                    {
+                        await Task.Delay(10);
+                        continue;
+                    }
+                    
+                    foreach (var pageSnapshotIndex in missingSnapshots.Except(requestedRenderings).ToList())
+                    {
+                        requestedRenderings.Add(pageSnapshotIndex);
+                        CommunicationService.Instance.RequestNewPage(new PageSnapshotIndex
+                        {
+                            PageIndex = pageSnapshotIndex.Item1,
+                            ZoomLevel = pageSnapshotIndex.Item2
+                        });
+                    }
+                }
+            });
             
             
-            CommunicationService.Instance.OnPageSnapshotsProvided += snapshots =>
+            CommunicationService.Instance.OnPageSnapshotsProvided += snapshot =>
             {
             {
-                InteractiveCanvas.AddSnapshots(snapshots);
+                InteractiveCanvas.AddSnapshots(snapshot);
                 Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
                 Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
             };
             };
 
 

+ 0 - 1
Source/QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -35,7 +35,6 @@
     </ItemGroup>
     </ItemGroup>
     
     
     <ItemGroup>
     <ItemGroup>
-        <FrameworkReference Include="Microsoft.AspNetCore.App" />
         <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
         <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
         <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
         <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
         <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
         <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />

+ 112 - 0
Source/QuestPDF.Previewer/SocketClient.cs

@@ -0,0 +1,112 @@
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace QuestPDF.Previewer;
+
+internal class SocketClient
+{
+    private TcpClient Client { get; set; }
+    private NetworkStream Stream;
+
+    private ConcurrentQueue<string> OutgoingMessages { get; } = new();
+    private ConcurrentQueue<string> IncomingMessages { get; } = new();
+    
+    public event Func<string, Task>? OnMessageReceived;
+    
+    public SocketClient(string ipAddress, int port)
+    {
+        Client = new TcpClient();
+        Client.Connect(IPAddress.Parse(ipAddress), port);
+        Stream = Client.GetStream();
+    }
+    
+    public SocketClient(TcpClient client)
+    {
+        Client = client;
+        Stream = client.GetStream();
+    }
+
+    public Task StartCommunication(CancellationToken cancellationToken = default)
+    {
+        var taskWorkers = Enumerable
+            .Range(0, Environment.ProcessorCount)
+            .Select(_ => Task.Run(() => HandleTaskReceivers(cancellationToken)))
+            .ToList();
+        
+        var runningTasks = new List<Task>
+        {
+            Task.Run(() => HandleIncomingMessages(cancellationToken)),
+            Task.Run(() => HandleOutgoingMessages(cancellationToken))
+        };
+        
+        return Task.WhenAll(taskWorkers.Concat(runningTasks));
+    }
+    
+    public void SendMessage(string message)
+    {
+        OutgoingMessages.Enqueue(message);
+    }
+
+    private async Task HandleIncomingMessages(CancellationToken cancellationToken = default)
+    {
+        using var binaryStream = new BinaryReader(Stream);
+        
+        while (Client.Connected)
+        {
+            var messageLength = binaryStream.ReadInt32();
+            var messageBytes = new byte[messageLength];
+                
+            await Stream.ReadExactlyAsync(messageBytes, 0, messageBytes.Length, cancellationToken);
+                
+            var message = Encoding.UTF8.GetString(messageBytes);
+            IncomingMessages.Enqueue(message);
+        }
+    }
+    
+    private async Task HandleOutgoingMessages(CancellationToken cancellationToken = default)
+    {
+        while (Client.Connected)
+        {
+            if (!OutgoingMessages.TryDequeue(out var message))
+            {
+                await Task.Delay(10, cancellationToken);
+                continue;
+            }
+            
+            Console.WriteLine($"Sending message: {message}");
+            
+            var data = Encoding.UTF8.GetBytes(message);
+            var length = BitConverter.GetBytes(data.Length);
+            
+            await Stream.WriteAsync(length, 0, length.Length, cancellationToken);
+            await Stream.WriteAsync(data, 0, data.Length, cancellationToken);
+        }
+    }
+    
+    private async Task HandleTaskReceivers(CancellationToken cancellationToken = default)
+    {
+        while (Client.Connected)
+        {
+            if (!IncomingMessages.TryDequeue(out var message))
+            {
+                await Task.Delay(10, cancellationToken);
+                continue;
+            }
+            
+            if (OnMessageReceived == null)
+                continue;
+            
+            await OnMessageReceived.Invoke(message);
+        }
+    }
+
+    public void Close()
+    {
+        OnMessageReceived = null;
+        
+        Stream.Close();
+        Client.Close();
+    }
+}

+ 7 - 0
Source/QuestPDF.Previewer/SocketMessage.cs

@@ -0,0 +1,7 @@
+namespace QuestPDF.Previewer;
+
+public class SocketMessage<TPayload>
+{
+    public string Channel { get; set; }
+    public TPayload Payload { get; set; }
+}

+ 49 - 0
Source/QuestPDF.Previewer/SocketServer.cs

@@ -0,0 +1,49 @@
+using System.Net;
+using System.Net.Sockets;
+
+namespace QuestPDF.Previewer;
+
+internal class SocketServer
+{
+    private TcpListener Listener { get; }
+    private bool IsServerRunning { get; set; }
+    
+    private SocketClient Client { get; set; }
+    public event Func<string, Task>? OnMessageReceived;
+
+    public SocketServer(string ipAddress, int port)
+    {
+        Listener = new TcpListener(IPAddress.Parse(ipAddress), port);
+    }
+
+    public void Start()
+    {
+        Listener.Start();
+        IsServerRunning = true;
+        
+        Task.Run(() => ListenForClients());
+    }
+
+    public void Stop()
+    {
+        IsServerRunning = false;
+        Listener.Stop();
+    }
+
+    private async Task ListenForClients()
+    {
+        while (IsServerRunning)
+        {
+            var client = await Listener.AcceptTcpClientAsync();
+            Client?.Close();
+            Client = new SocketClient(client);
+            Client.OnMessageReceived += async message => await OnMessageReceived.Invoke(message);
+            Client.StartCommunication();
+        }
+    }
+    
+    public void SendMessage(string message)
+    {
+        Client.SendMessage(message);
+    }
+}

+ 1 - 1
Source/QuestPDF.ReportSample/Layouts/StandardReport.cs

@@ -56,7 +56,7 @@ namespace QuestPDF.ReportSample.Layouts
                 {
                 {
                     row.Spacing(50);
                     row.Spacing(50);
                     
                     
-                    row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title);
+                    row.RelativeItem().PaddingTop(-10).Text("Test dovedností mladých hasičů").Style(Typography.Title);
                     row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
                     row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
                 });
                 });
 
 

+ 2 - 1
Source/QuestPDF.ReportSample/Tests.cs

@@ -6,6 +6,7 @@ using NUnit.Framework;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Previewer;
 using QuestPDF.ReportSample.Layouts;
 using QuestPDF.ReportSample.Layouts;
 
 
 namespace QuestPDF.ReportSample
 namespace QuestPDF.ReportSample
@@ -28,7 +29,7 @@ namespace QuestPDF.ReportSample
         [Test] 
         [Test] 
         public void GeneratePdfAndShow()
         public void GeneratePdfAndShow()
         {
         {
-            Report.GeneratePdfAndShow();
+            Report.ShowInPreviewer();
         }
         }
         
         
         [Test] 
         [Test] 

+ 7 - 0
Source/QuestPDF/Previewer/PreviewerModels.cs

@@ -25,4 +25,11 @@ class PageSnapshotIndex
     public override string ToString() => $"{ZoomLevel}/{PageIndex}";
     public override string ToString() => $"{ZoomLevel}/{PageIndex}";
 }
 }
 
 
+class PageSnapshotCommunicationData
+{
+    public int PageIndex { get; set; }
+    public int ZoomLevel { get; set; }
+    public byte[] ImageData { get; set; }
+}
+
 #endif
 #endif

+ 82 - 64
Source/QuestPDF/Previewer/PreviewerService.cs

@@ -6,6 +6,7 @@ using System.Diagnostics;
 using System.Linq;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http;
 using System.Net.Http.Json;
 using System.Net.Http.Json;
+using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
@@ -15,7 +16,7 @@ namespace QuestPDF.Previewer
     internal class PreviewerService
     internal class PreviewerService
     {
     {
         private int Port { get; }
         private int Port { get; }
-        private HttpClient HttpClient { get; }
+        private SocketClient SocketClient { get; }
         
         
         public event Action? OnPreviewerStopped;
         public event Action? OnPreviewerStopped;
 
 
@@ -26,12 +27,11 @@ namespace QuestPDF.Previewer
         
         
         public PreviewerService(int port)
         public PreviewerService(int port)
         {
         {
-            Port = port;
-            HttpClient = new()
-            {
-                BaseAddress = new Uri($"http://localhost:{port}/"), 
-                Timeout = TimeSpan.FromSeconds(1)
-            };
+            SocketClient = new SocketClient("127.0.0.1", port);
+            
+            
+            
+            SocketClient.StartCommunication();
         }
         }
 
 
         public async Task Connect()
         public async Task Connect()
@@ -50,21 +50,58 @@ namespace QuestPDF.Previewer
 
 
         private async Task<bool> IsPreviewerAvailable()
         private async Task<bool> IsPreviewerAvailable()
         {
         {
-            try
+            var isPreviewerAvailable = false;
+
+            SocketClient.OnMessageReceived += async message =>
             {
             {
-                using var result = await HttpClient.GetAsync("/ping");
-                return result.IsSuccessStatusCode;
-            }
-            catch
+                var content = JsonDocument.Parse(message).RootElement;
+                var channel = content.GetProperty("Channel").GetString();
+
+                if (channel == "ping/ok")
+                    isPreviewerAvailable = true;
+            };
+            
+            var request = new SocketMessage<PageSnapshotCommunicationData>
+            {
+                Channel = "ping/check"
+            };
+            
+            SocketClient.SendMessage(JsonSerializer.Serialize(request));
+            
+            while (!isPreviewerAvailable)
             {
             {
-                return false;
+                await Task.Delay(TimeSpan.FromMilliseconds(100));
             }
             }
+
+            return true;
         }
         }
         
         
         private async Task<Version> GetPreviewerVersion()
         private async Task<Version> GetPreviewerVersion()
         {
         {
-            using var result = await HttpClient.GetAsync("/version");
-            return await result.Content.ReadFromJsonAsync<Version>();
+            Version previewerVersion = default;
+            
+            SocketClient.OnMessageReceived += async message =>
+            {
+                var content = JsonDocument.Parse(message).RootElement;
+                var channel = content.GetProperty("Channel").GetString();
+
+                if (channel == "version/provide")
+                    previewerVersion = content.GetProperty("Payload").Deserialize<Version>();
+            };
+            
+            var request = new SocketMessage<PageSnapshotCommunicationData>
+            {
+                Channel = "version/check"
+            };
+            
+            SocketClient.SendMessage(JsonSerializer.Serialize(request));
+
+            while (previewerVersion == default)
+            {
+                await Task.Delay(TimeSpan.FromMilliseconds(100));
+            }
+
+            return previewerVersion;
         }
         }
         
         
         private void StartPreviewer()
         private void StartPreviewer()
@@ -164,67 +201,48 @@ namespace QuestPDF.Previewer
                     .ToArray()
                     .ToArray()
             };
             };
             
             
-            await HttpClient.PostAsync("/preview/update", JsonContent.Create(documentStructure));
+            var response = new SocketMessage<DocumentStructure>
+            {
+                Channel = "preview/update",
+                Payload = documentStructure
+            };
+                
+            SocketClient.SendMessage(JsonSerializer.Serialize(response));
         }
         }
         
         
         public void StartRenderRequestedPageSnapshotsTask(CancellationToken cancellationToken)
         public void StartRenderRequestedPageSnapshotsTask(CancellationToken cancellationToken)
         {
         {
-            Task.Run(async () =>
-            {
-                while (true)
-                {
-                    try
-                    {
-                        await RenderRequestedPageSnapshots();
-                    }
-                    catch
-                    {
-                        
-                    }
-                    
-                    await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
-                }
-            });
+            RenderRequestedPageSnapshots();
         }
         }
         
         
         private async Task RenderRequestedPageSnapshots()
         private async Task RenderRequestedPageSnapshots()
         {
         {
-            // get requests
-            var getRequestedSnapshots = await HttpClient.GetAsync("/preview/getRenderingRequests");
-            getRequestedSnapshots.EnsureSuccessStatusCode();
-            
-            var requestedSnapshots = await getRequestedSnapshots.Content.ReadFromJsonAsync<ICollection<PageSnapshotIndex>>();
-            
-            if (!requestedSnapshots.Any())
-                return;
-            
-            if (CurrentDocumentSnapshot == null)
-                return;
-      
-            // render snapshots
-            using var multipartContent = new MultipartFormDataContent();
-
-            var renderingTasks = requestedSnapshots
-                .Select(async index =>
-                {
-                    var image = CurrentDocumentSnapshot
-                        .Pictures
-                        .ElementAt(index.PageIndex)
-                        .RenderImage(index.ZoomLevel);
+            SocketClient.OnMessageReceived += async message =>
+            {
+                var content = JsonDocument.Parse(message).RootElement;
+                var channel = content.GetProperty("Channel").GetString();
 
 
-                    return (index, image);
-                })
-                .ToList();
+                if (channel != "preview/requestPage")
+                    return;
 
 
-            var renderedSnapshots = await Task.WhenAll(renderingTasks);
-            
-            // prepare response and send
-            foreach (var (index, image) in renderedSnapshots)
-                multipartContent.Add(new ByteArrayContent(image), index.ToString(), index.ToString());
+                var page = content.GetProperty("Payload").Deserialize<PageSnapshotIndex>();
 
 
-            multipartContent.Add(JsonContent.Create(requestedSnapshots), "metadata");
+                var image = CurrentDocumentSnapshot
+                    .Pictures
+                    .ElementAt(page.PageIndex)
+                    .RenderImage(page.ZoomLevel);
 
 
-            await HttpClient.PostAsync("/preview/provideRenderedImages", multipartContent);
+                var response = new SocketMessage<PageSnapshotCommunicationData>
+                {
+                    Channel = "preview/updatePage",
+                    Payload = new PageSnapshotCommunicationData
+                    {
+                        PageIndex = page.PageIndex, ZoomLevel = page.ZoomLevel, ImageData = image
+                    }
+                };
+                
+                SocketClient.SendMessage(JsonSerializer.Serialize(response));
+            };
         }
         }
     }
     }
 }
 }

+ 139 - 0
Source/QuestPDF/Previewer/SocketClient.cs

@@ -0,0 +1,139 @@
+#if NET6_0_OR_GREATER
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace QuestPDF.Previewer;
+
+internal class SocketClient
+{
+    private TcpClient Client { get; set; }
+    private NetworkStream Stream;
+
+    private ConcurrentQueue<string> OutgoingMessages { get; } = new();
+    private ConcurrentQueue<string> IncomingMessages { get; } = new();
+    
+    public event Func<string, Task>? OnMessageReceived;
+    
+    public SocketClient(string ipAddress, int port)
+    {
+        Client = new TcpClient();
+        Client.Connect(IPAddress.Parse(ipAddress), port);
+        Stream = Client.GetStream();
+    }
+    
+    public SocketClient(TcpClient client)
+    {
+        Client = client;
+        Stream = client.GetStream();
+    }
+
+    public Task StartCommunication(CancellationToken cancellationToken = default)
+    {
+        var taskWorkers = Enumerable
+            .Range(0, Environment.ProcessorCount)
+            .Select(_ => Task.Run(() => HandleTaskReceivers(cancellationToken)))
+            .ToList();
+        
+        var runningTasks = new List<Task>
+        {
+            Task.Run(() => HandleIncomingMessages(cancellationToken)),
+            Task.Run(() => HandleOutgoingMessages(cancellationToken))
+        };
+        
+        return Task.WhenAll(taskWorkers.Concat(runningTasks));
+    }
+    
+    public void SendMessage(string message)
+    {
+        OutgoingMessages.Enqueue(message);
+    }
+
+    private async Task HandleIncomingMessages(CancellationToken cancellationToken = default)
+    {
+        using var binaryStream = new BinaryReader(Stream);
+        
+        while (Client.Connected)
+        {
+            var messageLength = binaryStream.ReadInt32();
+            var messageBytes = await ReadExactlyAsync(Stream, messageLength, cancellationToken);
+                
+            var message = Encoding.UTF8.GetString(messageBytes);
+            IncomingMessages.Enqueue(message);
+        }
+        
+        static async Task<byte[]> ReadExactlyAsync(Stream stream, int bytesToRead, CancellationToken cancellationToken = default)
+        {
+            var buffer = new byte[bytesToRead];
+            
+            var bytesRead = 0;
+
+            while (bytesRead < bytesToRead)
+            {
+                var readBytes = await stream.ReadAsync(buffer, bytesRead, bytesToRead - bytesRead, cancellationToken);
+                
+                if (readBytes == 0)
+                    throw new EndOfStreamException("Reached end of stream before reading the required number of bytes.");
+
+                bytesRead += readBytes;
+            }
+
+            return buffer;
+        }
+    }
+    
+    private async Task HandleOutgoingMessages(CancellationToken cancellationToken = default)
+    {
+        while (Client.Connected)
+        {
+            if (!OutgoingMessages.TryDequeue(out var message))
+            {
+                await Task.Delay(10, cancellationToken);
+                continue;
+            }
+            
+            Console.WriteLine($"Sending message: {message}");
+            
+            var data = Encoding.UTF8.GetBytes(message);
+            var length = BitConverter.GetBytes(data.Length);
+            
+            await Stream.WriteAsync(length, 0, length.Length, cancellationToken);
+            await Stream.WriteAsync(data, 0, data.Length, cancellationToken);
+        }
+    }
+    
+    private async Task HandleTaskReceivers(CancellationToken cancellationToken = default)
+    {
+        while (Client.Connected)
+        {
+            if (!IncomingMessages.TryDequeue(out var message))
+            {
+                await Task.Delay(10, cancellationToken);
+                continue;
+            }
+            
+            if (OnMessageReceived == null)
+                continue;
+            
+            await OnMessageReceived.Invoke(message);
+        }
+    }
+
+    public void Close()
+    {
+        OnMessageReceived = null;
+        
+        Stream.Close();
+        Client.Close();
+    }
+}
+
+#endif

+ 7 - 0
Source/QuestPDF/Previewer/SocketMessage.cs

@@ -0,0 +1,7 @@
+namespace QuestPDF.Previewer;
+
+public class SocketMessage<TPayload>
+{
+    public string Channel { get; set; }
+    public TPayload Payload { get; set; }
+}