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

View File

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

View File

@@ -584,6 +584,32 @@ body.resizing {
justify-content: flex-start; 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 { .pdf-page-container {
position: relative; position: relative;
display: inline-block; display: inline-block;
@@ -641,6 +667,11 @@ body.resizing {
z-index: 20; z-index: 20;
} }
.pdf-signature-layer--dx {
width: 100%;
height: 100%;
}
.pdf-signature-layer .signature-button { .pdf-signature-layer .signature-button {
pointer-events: auto; pointer-events: auto;
} }