Replace PDF.js with DevExpress DxPdfViewer

This commit replaces the existing PDF.js-based viewer with the DevExpress DxPdfViewer component, introducing significant improvements to the UI, state management, and signature handling.

Key changes:
- Integrated DevExpress.Blazor.PdfViewer and removed PDF.js dependencies.
- Updated HTML structure to use `DxPdfViewer` and new overlay layers.
- Refactored zoom and navigation logic to use DevExpress APIs.
- Overhauled signature button rendering and positioning logic.
- Added dynamic scaling for applied signatures based on zoom level.
- Introduced `requestOverlayRefresh` for efficient overlay updates.
- Added new CSS styles for the DevExpress viewer and overlays.
- Refactored `pdf-viewer.js` to remove legacy PDF.js logic.
- Improved performance with `requestAnimationFrame` and optimized event handling.
- Added a `dispose` method for proper cleanup of resources.
- Enhanced error handling and accessibility for signature buttons.
- Removed redundant code and improved overall maintainability.
This commit is contained in:
2026-06-29 11:27:06 +02:00
parent a5e4f97397
commit db593cb46a
4 changed files with 769 additions and 992 deletions

View File

@@ -9,6 +9,7 @@
@using EnvelopeGenerator.Server.Client.Options
@using Microsoft.JSInterop
@using DevExpress.Blazor
@using DevExpress.Blazor.PdfViewer
@inject NavigationManager Navigation
@inject IOptions<PdfViewerOptions> PdfViewerOptions
@inject IJSRuntime JSRuntime
@@ -21,7 +22,6 @@
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="@AppVersion.GetVersionedUrl("js/pdf-viewer.js")"></script>
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
@@ -348,10 +348,15 @@
</div>
}
<div class="pdf-canvas-wrapper">
<div class="pdf-page-container">
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
<div id="pdf-text-layer" class="pdf-text-layer"></div>
<div id="pdf-signature-layer" class="pdf-signature-layer"></div>
<div id="pdf-dx-viewer-host" class="envelope-dx-viewer-host">
@if (_pdfDocumentContent is not null && _pdfDocumentContent.Length > 0)
{
<DxPdfViewer CssClass="envelope-dx-pdf-viewer"
DocumentContent="@_pdfDocumentContent"
ZoomLevel="@_currentZoom"
IsSinglePagePreview="true" />
}
<div id="pdf-signature-layer" class="pdf-signature-layer pdf-signature-layer--dx"></div>
</div>
</div>
</div>
@@ -548,6 +553,7 @@
bool _isLoading = true;
string? _errorMessage;
string? _pdfDataUrl;
byte[]? _pdfDocumentContent;
bool _pdfLoaded = false;
int _currentPage = 1;
int _totalPages = 0;
@@ -615,6 +621,7 @@
if (pdfBytes is { Length: > 0 })
{
_pdfDocumentContent = pdfBytes;
var base64 = Convert.ToBase64String(pdfBytes);
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
}
@@ -721,17 +728,18 @@
options.ZoomStepPercentage
});
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", _pdfDataUrl, "pdf-dx-viewer-host", "pdf-signature-layer", _dotNetRef);
if (success)
{
_pdfLoaded = true;
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
_currentPage = 1;
// Attach resize listeners
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
await JSRuntime.InvokeVoidAsync("pdfViewer.attachViewerInteractionListeners", "pdf-dx-viewer-host", _dotNetRef);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await InvokeAsync(StateHasChanged);
@@ -754,59 +762,44 @@
[JSInvokable]
public async Task OnZoomChanged(double scale)
{
_currentZoom = (int)(scale * 100);
await InvokeAsync(StateHasChanged);
// Small delay for canvas render to complete (reduced from 100ms to 10ms)
await Task.Delay(10);
await RenderSignatureButtonsAsync();
var requestedZoom = (int)Math.Round(scale * 100, MidpointRounding.AwayFromZero);
await SetZoom(requestedZoom);
}
async Task NextPage()
{
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage"))
{
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
await RenderSignatureButtonsAsync();
}
if (_currentPage >= _totalPages)
return;
_currentPage++;
await ApplyViewerStateAsync();
}
async Task PreviousPage()
{
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage"))
{
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
await RenderSignatureButtonsAsync();
}
if (_currentPage <= 1)
return;
_currentPage--;
await ApplyViewerStateAsync();
}
async Task ZoomIn()
{
if (_currentZoom >= 300) return;
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
// Update signature overlay positions after zoom
await RenderSignatureButtonsAsync();
await SetZoom(_currentZoom + PdfViewerOptions.Value.ZoomStepPercentage);
}
async Task ZoomOut()
{
if (_currentZoom <= 50) return;
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
// Update signature overlay positions after zoom
await RenderSignatureButtonsAsync();
await SetZoom(_currentZoom - PdfViewerOptions.Value.ZoomStepPercentage);
}
async Task SetZoom(int percentage)
{
var scale = percentage / 100.0;
await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale);
_currentZoom = percentage;
_currentZoom = Math.Clamp(percentage, 50, 300);
await ApplyViewerStateAsync();
}
async Task OnZoomSliderChanged(ChangeEventArgs e)
@@ -823,19 +816,16 @@
async Task OnPageInputChanged(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages)
{
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
{
_currentPage = pageNum;
}
await ApplyViewerStateAsync();
}
}
async Task FitToWidth()
{
await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
_currentZoom = 150;
await ApplyViewerStateAsync();
}
async Task ToggleThumbnails()
@@ -853,11 +843,11 @@
async Task GoToPageFromThumbnail(int pageNum)
{
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
{
if (pageNum < 1 || pageNum > _totalPages)
return;
_currentPage = pageNum;
await RenderSignatureButtonsAsync();
}
await ApplyViewerStateAsync();
}
async Task RenderSignatureButtonsAsync()
@@ -866,6 +856,7 @@
try
{
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
await UpdateSignatureCounterAsync();
}
@@ -906,7 +897,13 @@
public async Task OnPageChangedBySignatureNav(int newPage)
{
_currentPage = newPage;
await RenderSignatureButtonsAsync();
await ApplyViewerStateAsync();
}
[JSInvokable]
public async Task OnZoomGestureRequested(int zoomPercentage)
{
await SetZoom(zoomPercentage);
}
async Task UpdateSignatureCounterAsync()
@@ -1189,6 +1186,17 @@
await InvokeAsync(StateHasChanged);
}
async Task ApplyViewerStateAsync()
{
if (!_pdfLoaded)
return;
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
}
public async ValueTask DisposeAsync()
{
if (_pdfLoaded)

View File

@@ -10,3 +10,4 @@
@using EnvelopeGenerator.Server.Client
@using EnvelopeGenerator.Server.Components
@using DevExpress.Blazor
@using DevExpress.Blazor.PdfViewer

View File

@@ -584,6 +584,32 @@ body.resizing {
justify-content: flex-start;
}
.envelope-dx-viewer-host {
position: relative;
width: 100%;
height: 100%;
min-height: 720px;
background: #fff;
border-radius: 12px;
overflow: hidden;
}
.envelope-dx-pdf-viewer {
width: 100%;
height: 100%;
min-height: 720px;
}
.envelope-dx-pdf-viewer .dxbl-pdf-viewer,
.envelope-dx-pdf-viewer .dxbl-pdfviewer,
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-container,
.envelope-dx-pdf-viewer .dxbl-scroll-viewer,
.envelope-dx-pdf-viewer .dxbl-scroll-viewer-content,
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-content,
.envelope-dx-pdf-viewer .dxbl-pdfviewer-content {
height: 100%;
}
.pdf-page-container {
position: relative;
display: inline-block;
@@ -641,6 +667,11 @@ body.resizing {
z-index: 20;
}
.pdf-signature-layer--dx {
width: 100%;
height: 100%;
}
.pdf-signature-layer .signature-button {
pointer-events: auto;
}