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.
This commit is contained in:
2026-10-01 12:05:56 +02:00
parent ad7686b7c2
commit 819bdcfcfb

View File

@@ -4,7 +4,6 @@
@using EnvelopeGenerator.Domain.Constants @using EnvelopeGenerator.Domain.Constants
@using EnvelopeGenerator.Server.Client.Models @using EnvelopeGenerator.Server.Client.Models
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.Blazor.PdfViewer
@using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto @using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Common.Dto.History @using EnvelopeGenerator.Application.Common.Dto.History
@@ -13,6 +12,7 @@
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService @inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService @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.AuthService AuthService
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService @inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService @inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
@@ -465,10 +465,15 @@
else if (_previewPdfBytes is not null && _previewViewerMounted) else if (_previewPdfBytes is not null && _previewViewerMounted)
{ {
<div class="sender-preview-viewer-wrap"> <div class="sender-preview-viewer-wrap">
<DxPdfViewer DocumentContent="@_previewPdfBytes" <SenderPdfImageEditor IsPlacementMode="false"
ZoomLevel="-2" PageCount="@_previewPageCount"
IsSinglePagePreview="true" Pages="@_previewPagePreviews"
CssClass="sender-preview-pdf-viewer" /> PdfDataUrl="@PreviewPdfDataUrl"
PdfFileName="@_previewFileName"
Fields="@_previewFieldOverlays"
EditorClick="OnPreviewEditorClickAsync"
RemoveField="OnPreviewRemoveFieldAsync"
MoveField="OnPreviewMoveFieldAsync" />
</div> </div>
} }
</BodyContentTemplate> </BodyContentTemplate>
@@ -542,7 +547,15 @@
private bool _previewLoading = false; private bool _previewLoading = false;
private bool _previewViewerMounted = false; private bool _previewViewerMounted = false;
private byte[]? _previewPdfBytes = null; private byte[]? _previewPdfBytes = null;
private int _previewPageCount = 0;
private List<PdfPageImageDto> _previewPagePreviews = [];
private string _previewFileName = "document.pdf";
private string? _previewErrorMessage = null; private string? _previewErrorMessage = null;
private readonly IReadOnlyList<SenderPdfFieldOverlay> _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. // Phase 6e (step 1): UI state only. Export logic is added in step 2.
private string _selectedExportFormat = "xlsx"; private string _selectedExportFormat = "xlsx";
@@ -960,6 +973,9 @@
// Unmount the viewer first — let the previous instance fully dispose // Unmount the viewer first — let the previous instance fully dispose
_previewViewerMounted = false; _previewViewerMounted = false;
_previewPdfBytes = null; _previewPdfBytes = null;
_previewPageCount = 0;
_previewPagePreviews = [];
_previewFileName = $"envelope-{envelope.Id}.pdf";
_previewErrorMessage = null; _previewErrorMessage = null;
_previewLoading = true; _previewLoading = true;
_previewVisible = true; _previewVisible = true;
@@ -968,6 +984,29 @@
try try
{ {
_previewPdfBytes = await DocumentService.GetSenderDocumentBytesByEnvelopeIdAsync(envelope.Id); _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 // PDF ready — mount the viewer now
_previewViewerMounted = true; _previewViewerMounted = true;
@@ -1002,12 +1041,60 @@
{ {
await Task.Yield(); // wait one render cycle for the viewer to be removed from the DOM await Task.Yield(); // wait one render cycle for the viewer to be removed from the DOM
_previewPdfBytes = null; _previewPdfBytes = null;
_previewPageCount = 0;
_previewPagePreviews = [];
_previewFileName = "document.pdf";
_previewErrorMessage = null; _previewErrorMessage = null;
_previewLoading = false; _previewLoading = false;
await InvokeAsync(StateHasChanged); 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 ─────────────────────────────────────────────── // ── Grid Layout Persistence ───────────────────────────────────────────────
// LayoutAutoLoading fires when the grid initializes — restore saved layout. // LayoutAutoLoading fires when the grid initializes — restore saved layout.
// LayoutAutoSaving fires when the user changes column order/width/sort — persist it. // LayoutAutoSaving fires when the user changes column order/width/sort — persist it.