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.
This commit is contained in:
64
EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs
Normal file
64
EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
namespace EnvelopeGenerator.ReceiverUI.Options;
|
||||||
|
|
||||||
|
public class PdfViewerOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "PdfViewer";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base scale for thumbnail rendering (0.2 - 1.5 recommended)
|
||||||
|
/// Higher values = better quality but slower rendering
|
||||||
|
/// Default: 0.75
|
||||||
|
/// </summary>
|
||||||
|
public double ThumbnailBaseScale { get; set; } = 0.75;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable HiDPI/Retina support for thumbnails
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0)
|
||||||
|
/// Caps DPR to avoid excessive memory usage on 4K+ displays
|
||||||
|
/// Default: 2.0
|
||||||
|
/// </summary>
|
||||||
|
public double ThumbnailMaxDPR { get; set; } = 2.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable HiDPI/Retina support for main PDF canvas
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0)
|
||||||
|
/// Default: 2.0
|
||||||
|
/// </summary>
|
||||||
|
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable smooth zoom transition (fade effect)
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSmoothZoom { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zoom transition duration in milliseconds (50 - 500)
|
||||||
|
/// Default: 150
|
||||||
|
/// </summary>
|
||||||
|
public int ZoomTransitionDuration { get; set; } = 150;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opacity during rendering (0.0 - 1.0)
|
||||||
|
/// Lower values = more visible fade effect
|
||||||
|
/// Default: 0.85
|
||||||
|
/// </summary>
|
||||||
|
public double RenderingOpacity { get; set; } = 0.85;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delay between thumbnail renders in milliseconds (10 - 200)
|
||||||
|
/// Higher values = less browser stress, slower initial load
|
||||||
|
/// Default: 50
|
||||||
|
/// </summary>
|
||||||
|
public int ThumbnailRenderDelay { get; set; } = 50;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
@inject DocumentService DocumentService
|
@inject DocumentService DocumentService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IOptions<ApiOptions> AppOptions
|
@inject IOptions<ApiOptions> AppOptions
|
||||||
|
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject AnnotationService AnnotService
|
@inject AnnotationService AnnotService
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
@@ -236,6 +237,21 @@ protected override async Task OnInitializedAsync() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_dotNetRef = DotNetObjectReference.Create(this);
|
_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<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
|
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -337,13 +353,15 @@ protected override async Task OnInitializedAsync() {
|
|||||||
|
|
||||||
async Task RenderThumbnailsAsync() {
|
async Task RenderThumbnailsAsync() {
|
||||||
try {
|
try {
|
||||||
|
var delay = PdfViewerOptions.Value.ThumbnailRenderDelay;
|
||||||
|
|
||||||
// Sequential rendering to avoid overwhelming the browser
|
// Sequential rendering to avoid overwhelming the browser
|
||||||
for (int i = 1; i <= _totalPages; i++) {
|
for (int i = 1; i <= _totalPages; i++) {
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderThumbnail", i, $"thumb-canvas-{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) {
|
if (i < _totalPages) {
|
||||||
await Task.Delay(50);
|
await Task.Delay(delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
|
|||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
builder.Services.Configure<ApiOptions>(opts =>
|
builder.Services.Configure<ApiOptions>(opts =>
|
||||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
||||||
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.DocumentService>();
|
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.DocumentService>();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AuthService>();
|
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AuthService>();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AnnotationService>();
|
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AnnotationService>();
|
||||||
|
|||||||
@@ -2,5 +2,16 @@
|
|||||||
"Api": {
|
"Api": {
|
||||||
"BaseUrl": "",
|
"BaseUrl": "",
|
||||||
"ForceToUseFakeDocument": false
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,27 @@ window.pdfViewer = {
|
|||||||
currentRenderTask: null,
|
currentRenderTask: null,
|
||||||
dotNetReference: null,
|
dotNetReference: null,
|
||||||
wheelEventAttached: false,
|
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) {
|
async initialize(canvasId, pdfDataUrl, dotNetRef) {
|
||||||
try {
|
try {
|
||||||
@@ -98,8 +119,10 @@ window.pdfViewer = {
|
|||||||
async renderPage(num) {
|
async renderPage(num) {
|
||||||
this.pageRendering = true;
|
this.pageRendering = true;
|
||||||
|
|
||||||
// Add rendering class for smooth transition
|
// Add rendering class for smooth transition (if enabled)
|
||||||
this.canvas.classList.add('rendering');
|
if (this.qualityOptions.enableSmoothZoom) {
|
||||||
|
this.canvas.classList.add('rendering');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get scroll container
|
// Get scroll container
|
||||||
@@ -120,9 +143,11 @@ window.pdfViewer = {
|
|||||||
|
|
||||||
const page = await this.pdfDoc.getPage(num);
|
const page = await this.pdfDoc.getPage(num);
|
||||||
|
|
||||||
// HiDPI support for main canvas
|
// HiDPI support for main canvas (configurable)
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = this.qualityOptions.mainCanvasEnableHiDPI
|
||||||
const viewport = page.getViewport({ scale: this.scale * Math.min(dpr, 2) });
|
? Math.min(window.devicePixelRatio || 1, this.qualityOptions.mainCanvasMaxDPR)
|
||||||
|
: 1.0;
|
||||||
|
const viewport = page.getViewport({ scale: this.scale * dpr });
|
||||||
|
|
||||||
// Set internal canvas resolution (high quality)
|
// Set internal canvas resolution (high quality)
|
||||||
this.canvas.width = viewport.width;
|
this.canvas.width = viewport.width;
|
||||||
@@ -161,7 +186,9 @@ window.pdfViewer = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove rendering class after completion
|
// Remove rendering class after completion
|
||||||
this.canvas.classList.remove('rendering');
|
if (this.qualityOptions.enableSmoothZoom) {
|
||||||
|
this.canvas.classList.remove('rendering');
|
||||||
|
}
|
||||||
|
|
||||||
this.currentRenderTask = null;
|
this.currentRenderTask = null;
|
||||||
this.pageRendering = false;
|
this.pageRendering = false;
|
||||||
@@ -275,10 +302,12 @@ window.pdfViewer = {
|
|||||||
|
|
||||||
const page = await this.pdfDoc.getPage(pageNum);
|
const page = await this.pdfDoc.getPage(pageNum);
|
||||||
|
|
||||||
// High-quality rendering with HiDPI support
|
// High-quality rendering with HiDPI support (configurable)
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = this.qualityOptions.thumbnailEnableHiDPI
|
||||||
const baseScale = 0.75; // Increased base scale for better quality
|
? Math.min(window.devicePixelRatio || 1, this.qualityOptions.thumbnailMaxDPR)
|
||||||
const scale = baseScale * Math.min(dpr, 2); // Cap at 2x for performance
|
: 1.0;
|
||||||
|
const baseScale = this.qualityOptions.thumbnailBaseScale;
|
||||||
|
const scale = baseScale * dpr;
|
||||||
|
|
||||||
const viewport = page.getViewport({ scale: scale });
|
const viewport = page.getViewport({ scale: scale });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user