From 819bdcfcfbb75e6b4462bc6d998712a4d655c086 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 1 Oct 2026 12:05:56 +0200 Subject: [PATCH] Enhance PDF preview with rendering and editing support Replaced `DxPdfViewer` with `SenderPdfImageEditor` to enable PDF rendering, page previews, and field overlay editing. Added dependency injection for `PdfRenderService` and new state fields (`_previewPageCount`, `_previewPagePreviews`, `_previewFileName`, `_previewFieldOverlays`) to manage PDF metadata and interactions. Updated `OpenPreviewAsync` to render PDF pages using `PdfRenderService` and handle errors gracefully. Introduced `PreviewPdfDataUrl` for generating data URLs and a static helper `EstimatePdfPageCount` for fallback page count estimation. Added placeholder methods for user interactions with the editor. Improved cleanup logic to reset state fields when the viewer is unmounted. These changes enhance the PDF preview functionality while maintaining backward compatibility. --- .../Pages/EnvelopeSenderPage.razor | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor index b56dc33a..fdfbd7e0 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor @@ -4,7 +4,6 @@ @using EnvelopeGenerator.Domain.Constants @using EnvelopeGenerator.Server.Client.Models @using DevExpress.Blazor -@using DevExpress.Blazor.PdfViewer @using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Application.Common.Dto @using EnvelopeGenerator.Application.Common.Dto.History @@ -13,6 +12,7 @@ @using Microsoft.Extensions.Localization @inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService @inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService +@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService @inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService @inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService @inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService @@ -465,10 +465,15 @@ else if (_previewPdfBytes is not null && _previewViewerMounted) {
- +
} @@ -542,7 +547,15 @@ private bool _previewLoading = false; private bool _previewViewerMounted = false; private byte[]? _previewPdfBytes = null; + private int _previewPageCount = 0; + private List _previewPagePreviews = []; + private string _previewFileName = "document.pdf"; private string? _previewErrorMessage = null; + private readonly IReadOnlyList _previewFieldOverlays = []; + + private string? PreviewPdfDataUrl => _previewPdfBytes is { Length: > 0 } + ? $"data:application/pdf;base64,{Convert.ToBase64String(_previewPdfBytes)}" + : null; // Phase 6e (step 1): UI state only. Export logic is added in step 2. private string _selectedExportFormat = "xlsx"; @@ -960,6 +973,9 @@ // Unmount the viewer first — let the previous instance fully dispose _previewViewerMounted = false; _previewPdfBytes = null; + _previewPageCount = 0; + _previewPagePreviews = []; + _previewFileName = $"envelope-{envelope.Id}.pdf"; _previewErrorMessage = null; _previewLoading = true; _previewVisible = true; @@ -968,6 +984,29 @@ try { _previewPdfBytes = await DocumentService.GetSenderDocumentBytesByEnvelopeIdAsync(envelope.Id); + if (_previewPdfBytes is null || _previewPdfBytes.Length == 0) + { + _previewErrorMessage = L("DocumentCouldNotBeLoaded", "Dokument konnte nicht geladen werden"); + return; + } + + var previewPdfBytes = _previewPdfBytes; + + _previewPagePreviews = []; + try + { + _previewPagePreviews = (await PdfRenderService.RenderPagesAsync(previewPdfBytes)) + .OrderBy(p => p.PageNumber) + .ToList(); + } + catch (Exception renderEx) + { + Logger.LogWarning(renderEx, "Preview: page image rendering failed for envelope {EnvelopeId}. Falling back to native PDF preview.", envelope.Id); + } + + _previewPageCount = _previewPagePreviews.Count > 0 + ? _previewPagePreviews.Count + : EstimatePdfPageCount(previewPdfBytes) ?? 1; // PDF ready — mount the viewer now _previewViewerMounted = true; @@ -1002,12 +1041,60 @@ { await Task.Yield(); // wait one render cycle for the viewer to be removed from the DOM _previewPdfBytes = null; + _previewPageCount = 0; + _previewPagePreviews = []; + _previewFileName = "document.pdf"; _previewErrorMessage = null; _previewLoading = false; await InvokeAsync(StateHasChanged); }); } + Task OnPreviewEditorClickAsync(Microsoft.AspNetCore.Components.Web.MouseEventArgs _) + { + return Task.CompletedTask; + } + + Task OnPreviewRemoveFieldAsync(int _) + { + return Task.CompletedTask; + } + + Task OnPreviewMoveFieldAsync(SenderPdfFieldMoveRequest _) + { + return Task.CompletedTask; + } + + static int? EstimatePdfPageCount(byte[]? documentBytes) + { + if (documentBytes is null || documentBytes.Length == 0) + return null; + + try + { + var text = System.Text.Encoding.ASCII.GetString(documentBytes); + const string marker = "/Type /Page"; + var count = 0; + var index = 0; + + while (true) + { + index = text.IndexOf(marker, index, StringComparison.Ordinal); + if (index < 0) + break; + + count++; + index += marker.Length; + } + + return count > 0 ? count : null; + } + catch + { + return null; + } + } + // ── Grid Layout Persistence ─────────────────────────────────────────────── // LayoutAutoLoading fires when the grid initializes — restore saved layout. // LayoutAutoSaving fires when the user changes column order/width/sort — persist it.