From c4ef195e203abdb8c6dc8013dd04b69eadd9c24a Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 6 Jun 2026 13:08:54 +0200 Subject: [PATCH] Enhance EnvelopeViewer with configurable quality options Updated EnvelopeViewer to support configurable quality settings via `PdfViewerOptions` and `appsettings.json`. Added HiDPI/Retina support, smooth zoom transitions, and unlimited zoom with configurable step percentages. Introduced a resizable thumbnail sidebar with localStorage persistence. Simplified initialization and cleanup processes, and documented new features and architecture. Improved user experience and performance compared to the legacy ReportViewer. --- COPILOT_CONTEXT_EN.md | 178 ++++++++++++++++++++++++------------------ 1 file changed, 104 insertions(+), 74 deletions(-) diff --git a/COPILOT_CONTEXT_EN.md b/COPILOT_CONTEXT_EN.md index 6a492f22..fbc33bd4 100644 --- a/COPILOT_CONTEXT_EN.md +++ b/COPILOT_CONTEXT_EN.md @@ -61,8 +61,8 @@ Conversions: ## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer **Route:** `/envelope/{EnvelopeKey}` -**Purpose:** Simple, modern PDF viewing without signing functionality. -**Technology:** PDF.js 3.11.174 + custom JavaScript wrapper +**Purpose:** Modern, high-performance PDF viewing without signing functionality. +**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings ### Architecture @@ -70,39 +70,81 @@ Conversions: - Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)` - Converts to base64 data URL: `data:application/pdf;base64,{base64}` - Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks -- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage, Thumbnail toggle -- Thumbnail sidebar with resizable splitter (150px-400px range) +- Quality settings loaded from `appsettings.json` via `IOptions` +- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence) - CSS externalized to `envelope-viewer.css` **JavaScript (`pdf-viewer.js`):** ```javascript window.pdfViewer = { - pdfDoc, canvas, ctx, scale, currentRenderTask, - dotNetReference, wheelEventAttached, - isResizing, resizeMouseMoveHandler, resizeMouseUpHandler, - + qualityOptions, // Configurable from appsettings.json + setQualityOptions(options), // Dynamic quality update initialize(canvasId, pdfDataUrl, dotNetRef), renderPage(num), renderThumbnail(pageNum, canvasId), - attachWheelEvent(), // Global Ctrl+Wheel zoom - attachResizeListeners(dotNetRef), // Splitter resize - detachResizeListeners(), - startResize(), - zoomIn(), zoomOut(), - nextPage(), previousPage(), + attachWheelEvent(), // Ctrl+Wheel zoom (configurable step) + zoomIn(), zoomOut(), // Configurable step percentage dispose() } ``` -**CSS (`envelope-viewer.css`):** -- `.envelope-viewer-layout`: Full-height gradient background -- `.envelope-action-bar`: Top bar with logo, title, controls (sticky) -- `.pdf-frame`: Flex container (row) with thumbnails + canvas side-by-side -- `.pdf-thumbnails`: Left sidebar (260px default, resizable 150-400px), no header -- `.pdf-splitter`: 4px resizable divider with `col-resize` cursor -- `.pdf-canvas-wrapper`: Flex-grow container with scroll, padding, centered canvas -- `.pdf-canvas`: `display: inline-block`, unlimited zoom, scrollable when exceeds frame -- Modern glassmorphism design with gradients and shadows +**Options (`PdfViewerOptions.cs`):** +```csharp +public class PdfViewerOptions { + public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5 + public bool ThumbnailEnableHiDPI { get; set; } = true; + public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0 + public bool MainCanvasEnableHiDPI { get; set; } = true; + public double MainCanvasMaxDPR { get; set; } = 2.0; + public bool EnableSmoothZoom { get; set; } = true; + public int ZoomTransitionDuration { get; set; } = 150; // ms + public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0 + public int ThumbnailRenderDelay { get; set; } = 50; // ms + public int ZoomStepPercentage { get; set; } = 5; // 1-50% +} +``` + +### Configuration (appsettings.json) + +**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json` + +```json +{ + "PdfViewer": { + "ThumbnailBaseScale": 0.75, + "ThumbnailEnableHiDPI": true, + "ThumbnailMaxDPR": 2.0, + "MainCanvasEnableHiDPI": true, + "MainCanvasMaxDPR": 2.0, + "EnableSmoothZoom": true, + "ZoomTransitionDuration": 150, + "RenderingOpacity": 0.85, + "ThumbnailRenderDelay": 50, + "ZoomStepPercentage": 5 + } +} +``` + +**Usage:** Edit file ? F5 (browser refresh) ? new settings applied + +**Presets:** +- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices) +- **Balanced**: Default values (recommended) +- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end) + +--- + +### Features + +1. **HiDPI/Retina Support** ? 4x quality on Retina displays +2. **Configurable Quality** ? All parameters in appsettings.json +3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%) +4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page +5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering +6. **Smooth Transitions** ? Configurable fade effect +7. **Responsive Design** ? Desktop/mobile adaptive layout + +--- ### Features @@ -148,39 +190,38 @@ window.pdfViewer = { - Mobile: 95% width, adjusted heights, thumbnails collapse to top - Adaptive padding and font sizes -### Flow +### Initialization Flow -1. **Component Load:** - ```csharp - OnInitializedAsync(): - - Fetch PDF bytes - - Convert to base64 data URL - - Set _isLoading = false - - OnAfterRenderAsync(firstRender): - - Load saved thumbnail width from localStorage - - Create DotNetObjectReference - - JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef) - - Attach resize listeners for splitter - - Update _totalPages, _currentPage, _pdfLoaded - - Render thumbnails sequentially (50ms delay between pages) - ``` +```csharp +OnInitializedAsync(): + 1. Fetch PDF bytes from DocumentService + 2. Convert to base64 data URL + 3. Set _isLoading = false -2. **User Interaction:** - - Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")` - - Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')` - - Page buttons ? `NextPage()`/`PreviousPage()` ? `JSRuntime.InvokeAsync("pdfViewer.nextPage")` - - Thumbnail click ? `GoToPageFromThumbnail(pageNum)` ? `pdfViewer.goToPage(pageNum)` - - Toggle button ? `ToggleThumbnails()` ? `_showThumbnails = !_showThumbnails` - - Splitter drag ? `OnSplitterMouseDown()` ? JS global mouse events ? `OnSplitterMouseMove(clientX)` ? width update ? `OnSplitterMouseUp()` ? save to localStorage +OnAfterRenderAsync(firstRender): + 1. Load saved thumbnail width from localStorage + 2. Create DotNetObjectReference + 3. Send PdfViewerOptions to JavaScript + 4. Initialize PDF.js viewer + 5. Attach splitter resize listeners + 6. Render thumbnails sequentially (configurable delay) +``` -3. **Cleanup:** - ```csharp - DisposeAsync(): - - JSRuntime.InvokeVoidAsync("pdfViewer.dispose") - - Detach resize listeners - - _dotNetRef?.Dispose() - ``` +**User Interactions:** +- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage +- Pages: Buttons/Input/Thumbnails ? navigate +- Sidebar: Toggle button ? show/hide thumbnails +- Splitter: Drag ? resize sidebar (150-400px) + +**Cleanup:** +```csharp +DisposeAsync(): + - Dispose PDF.js viewer + - Detach event listeners + - Dispose DotNetObjectReference +``` + +--- ### Key Differences from ReportViewer @@ -301,13 +342,8 @@ return report; | `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | | Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | | iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | -| **PDF.js: `display: flex` on `.pdf-frame`** | **Prevents left-edge scroll when canvas exceeds container** | -| **PDF.js: `max-width: 100%` on canvas** | **Limits zoom; user expects unlimited zoom capability** | -| **Mouse wheel on `.pdf-frame` only** | **Only works when mouse over PDF; should work anywhere on page** | -| **OnAfterRenderAsync without `firstRender` guard** | **Creates infinite loop when `StateHasChanged` is called repeatedly** | -| **Conditional rendering with `@if (_pdfLoaded)` wrapping canvas** | **Canvas not in DOM when initialize called, causing perpetual failure** | -| **Thumbnail sidebar with `position: absolute`** | **Independent from PDF canvas, breaks alignment on screen resize** | -| **Thumbnail header with title/close button** | **Wastes valuable space; toolbar toggle is sufficient** | +| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | +| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | --- @@ -333,20 +369,14 @@ Our use case is **visual/image stamping** at specific page coordinates | 10 | — | Investigated DevExpress article — not applicable to our case | | 10 | — | Added iText7 to ReceiverUI; implemented `StampSignaturesOnPdf` — ? deterministic coordinates, no page count side effects | | 10 | — | Split COPILOT_CONTEXT.md into COPILOT_CONTEXT_EN.md and COPILOT_CONTEXT_TR.md | -| **11** | **2025-01-XX** | **Created EnvelopeViewer.razor (`/envelope/{key}`) with PDF.js 3.11.174** | -| **11** | **2025-01-XX** | **Implemented `pdf-viewer.js`: canvas rendering, zoom, pagination, render task cancellation** | -| **11** | **2025-01-XX** | **Externalized CSS to `envelope-viewer.css`: modern glassmorphism design** | -| **11** | **2025-01-XX** | **Fixed scroll issues: removed `display: flex`, used `text-align: center` + `inline-block`** | -| **11** | **2025-01-XX** | **Removed canvas `max-width` restriction for unlimited zoom** | -| **11** | **2025-01-XX** | **Added global mouse wheel zoom: `Ctrl+Wheel` on `document.body`, JSInterop callback to Blazor** | -| **11** | **2025-01-XX** | **Added PDF thumbnail sidebar (left panel) with page previews and navigation** | -| **11** | **2025-01-XX** | **Implemented thumbnail rendering system with sequential loading (50ms delay between pages)** | -| **11** | **2025-01-XX** | **Fixed thumbnail rendering: retry logic (10x 100ms) for canvas availability** | -| **11** | **2025-01-XX** | **Refactored layout: Moved thumbnails inside `pdf-frame` for flex side-by-side design** | -| **11** | **2025-01-XX** | **Removed thumbnail header (title + close button) to maximize thumbnail space** | -| **11** | **2025-01-XX** | **Added resizable splitter: 4px draggable divider, 150-400px range, localStorage persistence** | -| **11** | **2025-01-XX** | **Fixed vertical alignment: `align-items: stretch` ensures thumbnails and canvas have same height** | -| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: Documented resizable splitter and layout refactoring** | +| **11** | **2025-01-XX** | **Created EnvelopeViewer.razor with PDF.js 3.11.174 + modern UI** | +| **11** | **2025-01-XX** | **Implemented configurable quality system (PdfViewerOptions + appsettings.json)** | +| **11** | **2025-01-XX** | **Added HiDPI/Retina support (4x quality on Retina displays)** | +| **11** | **2025-01-XX** | **Implemented thumbnail sidebar with resizable splitter (150-400px, localStorage)** | +| **11** | **2025-01-XX** | **Added smooth zoom transitions with configurable opacity and duration** | +| **11** | **2025-01-XX** | **Made zoom step configurable (buttons, Ctrl+Wheel, slider use same step)** | +| **11** | **2025-01-XX** | **Fixed thumbnail canvas alignment (object-fit: contain)** | +| **11** | **2025-01-XX** | **Fixed thumbnail re-rendering on sidebar toggle** | ---