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 });