From 139b92ed8c8d1207102f34b67d722fe091af4d76 Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 6 Jun 2026 12:15:48 +0200 Subject: [PATCH] Add configurable PDF viewer options and improve rendering Introduced `PdfViewerOptions` class to centralize PDF viewer settings such as scaling, HiDPI support, zoom transitions, and rendering delays. Bound these options to `appsettings.json` for dynamic configuration. Injected `PdfViewerOptions` into `EnvelopeViewer.razor` and updated `OnInitializedAsync` to pass settings to JavaScript. Replaced hardcoded values in `pdf-viewer.js` with configurable options, improving maintainability and flexibility. Enhanced rendering logic to respect HiDPI settings, maximum DPR, and smooth zoom transitions. Improved thumbnail rendering with configurable delays to optimize performance. --- .../Options/PdfViewerOptions.cs | 64 +++++++++++++++++++ .../Pages/EnvelopeViewer.razor | 22 ++++++- EnvelopeGenerator.ReceiverUI/Program.cs | 2 + .../wwwroot/appsettings.json | 11 ++++ .../wwwroot/js/pdf-viewer.js | 49 +++++++++++--- 5 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs diff --git a/EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs b/EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs new file mode 100644 index 00000000..779ac6f3 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs @@ -0,0 +1,64 @@ +namespace EnvelopeGenerator.ReceiverUI.Options; + +public class PdfViewerOptions +{ + public const string SectionName = "PdfViewer"; + + /// + /// Base scale for thumbnail rendering (0.2 - 1.5 recommended) + /// Higher values = better quality but slower rendering + /// Default: 0.75 + /// + public double ThumbnailBaseScale { get; set; } = 0.75; + + /// + /// Enable HiDPI/Retina support for thumbnails + /// Default: true + /// + public bool ThumbnailEnableHiDPI { get; set; } = true; + + /// + /// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0) + /// Caps DPR to avoid excessive memory usage on 4K+ displays + /// Default: 2.0 + /// + public double ThumbnailMaxDPR { get; set; } = 2.0; + + /// + /// Enable HiDPI/Retina support for main PDF canvas + /// Default: true + /// + public bool MainCanvasEnableHiDPI { get; set; } = true; + + /// + /// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0) + /// Default: 2.0 + /// + public double MainCanvasMaxDPR { get; set; } = 2.0; + + /// + /// Enable smooth zoom transition (fade effect) + /// Default: true + /// + public bool EnableSmoothZoom { get; set; } = true; + + /// + /// Zoom transition duration in milliseconds (50 - 500) + /// Default: 150 + /// + public int ZoomTransitionDuration { get; set; } = 150; + + /// + /// Opacity during rendering (0.0 - 1.0) + /// Lower values = more visible fade effect + /// Default: 0.85 + /// + public double RenderingOpacity { get; set; } = 0.85; + + /// + /// Delay between thumbnail renders in milliseconds (10 - 200) + /// Higher values = less browser stress, slower initial load + /// Default: 50 + /// + public int ThumbnailRenderDelay { get; set; } = 50; +} diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor index 99c7b93b..dc4f7f2c 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor @@ -6,6 +6,7 @@ @inject DocumentService DocumentService @inject NavigationManager Navigation @inject IOptions AppOptions +@inject IOptions PdfViewerOptions @inject IJSRuntime JSRuntime @inject AnnotationService AnnotService @implements IAsyncDisposable @@ -236,6 +237,21 @@ protected override async Task OnInitializedAsync() { try { _dotNetRef = DotNetObjectReference.Create(this); + + // Send quality options to JavaScript + var options = PdfViewerOptions.Value; + await JSRuntime.InvokeVoidAsync("pdfViewer.setQualityOptions", new + { + options.ThumbnailBaseScale, + options.ThumbnailEnableHiDPI, + options.ThumbnailMaxDPR, + options.MainCanvasEnableHiDPI, + options.MainCanvasMaxDPR, + options.EnableSmoothZoom, + options.ZoomTransitionDuration, + options.RenderingOpacity + }); + var success = await JSRuntime.InvokeAsync("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef); if (success) { @@ -337,13 +353,15 @@ protected override async Task OnInitializedAsync() { async Task RenderThumbnailsAsync() { try { + var delay = PdfViewerOptions.Value.ThumbnailRenderDelay; + // Sequential rendering to avoid overwhelming the browser for (int i = 1; i <= _totalPages; i++) { await JSRuntime.InvokeVoidAsync("pdfViewer.renderThumbnail", i, $"thumb-canvas-{i}"); - // Small delay between renders to keep UI responsive + // Configurable delay between renders if (i < _totalPages) { - await Task.Delay(50); + await Task.Delay(delay); } } } catch (Exception ex) { diff --git a/EnvelopeGenerator.ReceiverUI/Program.cs b/EnvelopeGenerator.ReceiverUI/Program.cs index ceb3855e..bae5584a 100644 --- a/EnvelopeGenerator.ReceiverUI/Program.cs +++ b/EnvelopeGenerator.ReceiverUI/Program.cs @@ -14,6 +14,8 @@ builder.RootComponents.Add("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.Configure(opts => builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts)); +builder.Services.Configure(opts => +builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts)); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json b/EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json index fc7e14ce..447f6783 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json @@ -2,5 +2,16 @@ "Api": { "BaseUrl": "", "ForceToUseFakeDocument": false + }, + "PdfViewer": { + "ThumbnailBaseScale": 0.75, + "ThumbnailEnableHiDPI": true, + "ThumbnailMaxDPR": 2.0, + "MainCanvasEnableHiDPI": true, + "MainCanvasMaxDPR": 2.0, + "EnableSmoothZoom": true, + "ZoomTransitionDuration": 900, + "RenderingOpacity": 0.85, + "ThumbnailRenderDelay": 50 } } diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js index 3c677760..2a54ca70 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js @@ -11,6 +11,27 @@ window.pdfViewer = { currentRenderTask: null, dotNetReference: null, wheelEventAttached: false, + + // Quality options (configurable from appsettings.json) + qualityOptions: { + thumbnailBaseScale: 0.75, + thumbnailEnableHiDPI: true, + thumbnailMaxDPR: 2.0, + mainCanvasEnableHiDPI: true, + mainCanvasMaxDPR: 2.0, + enableSmoothZoom: true, + zoomTransitionDuration: 150, + renderingOpacity: 0.85 + }, + + setQualityOptions(options) { + this.qualityOptions = { ...this.qualityOptions, ...options }; + console.log('PDF Viewer quality options updated:', this.qualityOptions); + + // Apply CSS variables for dynamic styling + document.documentElement.style.setProperty('--zoom-transition-duration', `${options.zoomTransitionDuration}ms`); + document.documentElement.style.setProperty('--rendering-opacity', options.renderingOpacity); + }, async initialize(canvasId, pdfDataUrl, dotNetRef) { try { @@ -98,8 +119,10 @@ window.pdfViewer = { async renderPage(num) { this.pageRendering = true; - // Add rendering class for smooth transition - this.canvas.classList.add('rendering'); + // Add rendering class for smooth transition (if enabled) + if (this.qualityOptions.enableSmoothZoom) { + this.canvas.classList.add('rendering'); + } try { // Get scroll container @@ -120,9 +143,11 @@ window.pdfViewer = { const page = await this.pdfDoc.getPage(num); - // HiDPI support for main canvas - const dpr = window.devicePixelRatio || 1; - const viewport = page.getViewport({ scale: this.scale * Math.min(dpr, 2) }); + // HiDPI support for main canvas (configurable) + const dpr = this.qualityOptions.mainCanvasEnableHiDPI + ? Math.min(window.devicePixelRatio || 1, this.qualityOptions.mainCanvasMaxDPR) + : 1.0; + const viewport = page.getViewport({ scale: this.scale * dpr }); // Set internal canvas resolution (high quality) this.canvas.width = viewport.width; @@ -161,7 +186,9 @@ window.pdfViewer = { } // Remove rendering class after completion - this.canvas.classList.remove('rendering'); + if (this.qualityOptions.enableSmoothZoom) { + this.canvas.classList.remove('rendering'); + } this.currentRenderTask = null; this.pageRendering = false; @@ -275,10 +302,12 @@ window.pdfViewer = { const page = await this.pdfDoc.getPage(pageNum); - // High-quality rendering with HiDPI support - const dpr = window.devicePixelRatio || 1; - const baseScale = 0.75; // Increased base scale for better quality - const scale = baseScale * Math.min(dpr, 2); // Cap at 2x for performance + // High-quality rendering with HiDPI support (configurable) + const dpr = this.qualityOptions.thumbnailEnableHiDPI + ? Math.min(window.devicePixelRatio || 1, this.qualityOptions.thumbnailMaxDPR) + : 1.0; + const baseScale = this.qualityOptions.thumbnailBaseScale; + const scale = baseScale * dpr; const viewport = page.getViewport({ scale: scale });