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.
This commit is contained in:
@@ -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<PdfViewerOptions>`
|
||||
- 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** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user