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
|
## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
**Route:** `/envelope/{EnvelopeKey}`
|
||||||
**Purpose:** Simple, modern PDF viewing without signing functionality.
|
**Purpose:** Modern, high-performance PDF viewing without signing functionality.
|
||||||
**Technology:** PDF.js 3.11.174 + custom JavaScript wrapper
|
**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
@@ -70,39 +70,81 @@ Conversions:
|
|||||||
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
||||||
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
||||||
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
||||||
- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage, Thumbnail toggle
|
- Quality settings loaded from `appsettings.json` via `IOptions<PdfViewerOptions>`
|
||||||
- Thumbnail sidebar with resizable splitter (150px-400px range)
|
- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
|
||||||
- CSS externalized to `envelope-viewer.css`
|
- CSS externalized to `envelope-viewer.css`
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
**JavaScript (`pdf-viewer.js`):**
|
||||||
```javascript
|
```javascript
|
||||||
window.pdfViewer = {
|
window.pdfViewer = {
|
||||||
pdfDoc, canvas, ctx, scale, currentRenderTask,
|
qualityOptions, // Configurable from appsettings.json
|
||||||
dotNetReference, wheelEventAttached,
|
setQualityOptions(options), // Dynamic quality update
|
||||||
isResizing, resizeMouseMoveHandler, resizeMouseUpHandler,
|
|
||||||
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||||||
renderPage(num),
|
renderPage(num),
|
||||||
renderThumbnail(pageNum, canvasId),
|
renderThumbnail(pageNum, canvasId),
|
||||||
attachWheelEvent(), // Global Ctrl+Wheel zoom
|
attachWheelEvent(), // Ctrl+Wheel zoom (configurable step)
|
||||||
attachResizeListeners(dotNetRef), // Splitter resize
|
zoomIn(), zoomOut(), // Configurable step percentage
|
||||||
detachResizeListeners(),
|
|
||||||
startResize(),
|
|
||||||
zoomIn(), zoomOut(),
|
|
||||||
nextPage(), previousPage(),
|
|
||||||
dispose()
|
dispose()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**CSS (`envelope-viewer.css`):**
|
**Options (`PdfViewerOptions.cs`):**
|
||||||
- `.envelope-viewer-layout`: Full-height gradient background
|
```csharp
|
||||||
- `.envelope-action-bar`: Top bar with logo, title, controls (sticky)
|
public class PdfViewerOptions {
|
||||||
- `.pdf-frame`: Flex container (row) with thumbnails + canvas side-by-side
|
public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5
|
||||||
- `.pdf-thumbnails`: Left sidebar (260px default, resizable 150-400px), no header
|
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||||||
- `.pdf-splitter`: 4px resizable divider with `col-resize` cursor
|
public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0
|
||||||
- `.pdf-canvas-wrapper`: Flex-grow container with scroll, padding, centered canvas
|
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||||||
- `.pdf-canvas`: `display: inline-block`, unlimited zoom, scrollable when exceeds frame
|
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||||||
- Modern glassmorphism design with gradients and shadows
|
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
|
### Features
|
||||||
|
|
||||||
@@ -148,39 +190,38 @@ window.pdfViewer = {
|
|||||||
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
||||||
- Adaptive padding and font sizes
|
- Adaptive padding and font sizes
|
||||||
|
|
||||||
### Flow
|
### Initialization Flow
|
||||||
|
|
||||||
1. **Component Load:**
|
```csharp
|
||||||
```csharp
|
OnInitializedAsync():
|
||||||
OnInitializedAsync():
|
1. Fetch PDF bytes from DocumentService
|
||||||
- Fetch PDF bytes
|
2. Convert to base64 data URL
|
||||||
- Convert to base64 data URL
|
3. Set _isLoading = false
|
||||||
- Set _isLoading = false
|
|
||||||
|
|
||||||
OnAfterRenderAsync(firstRender):
|
OnAfterRenderAsync(firstRender):
|
||||||
- Load saved thumbnail width from localStorage
|
1. Load saved thumbnail width from localStorage
|
||||||
- Create DotNetObjectReference
|
2. Create DotNetObjectReference
|
||||||
- JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef)
|
3. Send PdfViewerOptions to JavaScript
|
||||||
- Attach resize listeners for splitter
|
4. Initialize PDF.js viewer
|
||||||
- Update _totalPages, _currentPage, _pdfLoaded
|
5. Attach splitter resize listeners
|
||||||
- Render thumbnails sequentially (50ms delay between pages)
|
6. Render thumbnails sequentially (configurable delay)
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **User Interaction:**
|
**User Interactions:**
|
||||||
- Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")`
|
- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage
|
||||||
- Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')`
|
- Pages: Buttons/Input/Thumbnails ? navigate
|
||||||
- Page buttons ? `NextPage()`/`PreviousPage()` ? `JSRuntime.InvokeAsync("pdfViewer.nextPage")`
|
- Sidebar: Toggle button ? show/hide thumbnails
|
||||||
- Thumbnail click ? `GoToPageFromThumbnail(pageNum)` ? `pdfViewer.goToPage(pageNum)`
|
- Splitter: Drag ? resize sidebar (150-400px)
|
||||||
- Toggle button ? `ToggleThumbnails()` ? `_showThumbnails = !_showThumbnails`
|
|
||||||
- Splitter drag ? `OnSplitterMouseDown()` ? JS global mouse events ? `OnSplitterMouseMove(clientX)` ? width update ? `OnSplitterMouseUp()` ? save to localStorage
|
|
||||||
|
|
||||||
3. **Cleanup:**
|
**Cleanup:**
|
||||||
```csharp
|
```csharp
|
||||||
DisposeAsync():
|
DisposeAsync():
|
||||||
- JSRuntime.InvokeVoidAsync("pdfViewer.dispose")
|
- Dispose PDF.js viewer
|
||||||
- Detach resize listeners
|
- Detach event listeners
|
||||||
- _dotNetRef?.Dispose()
|
- Dispose DotNetObjectReference
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Key Differences from ReportViewer
|
### Key Differences from ReportViewer
|
||||||
|
|
||||||
@@ -301,13 +342,8 @@ return report;
|
|||||||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM |
|
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM |
|
||||||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI |
|
| 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 |
|
| 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: Hardcoded quality values** | **Use appsettings.json for configurability** |
|
||||||
| **PDF.js: `max-width: 100%` on canvas** | **Limits zoom; user expects unlimited zoom capability** |
|
| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** |
|
||||||
| **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** |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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 | — | Investigated DevExpress article — not applicable to our case |
|
||||||
| 10 | — | Added iText7 to ReceiverUI; implemented `StampSignaturesOnPdf` — ? deterministic coordinates, no page count side effects |
|
| 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 |
|
| 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** | **Created EnvelopeViewer.razor with PDF.js 3.11.174 + modern UI** |
|
||||||
| **11** | **2025-01-XX** | **Implemented `pdf-viewer.js`: canvas rendering, zoom, pagination, render task cancellation** |
|
| **11** | **2025-01-XX** | **Implemented configurable quality system (PdfViewerOptions + appsettings.json)** |
|
||||||
| **11** | **2025-01-XX** | **Externalized CSS to `envelope-viewer.css`: modern glassmorphism design** |
|
| **11** | **2025-01-XX** | **Added HiDPI/Retina support (4x quality on Retina displays)** |
|
||||||
| **11** | **2025-01-XX** | **Fixed scroll issues: removed `display: flex`, used `text-align: center` + `inline-block`** |
|
| **11** | **2025-01-XX** | **Implemented thumbnail sidebar with resizable splitter (150-400px, localStorage)** |
|
||||||
| **11** | **2025-01-XX** | **Removed canvas `max-width` restriction for unlimited zoom** |
|
| **11** | **2025-01-XX** | **Added smooth zoom transitions with configurable opacity and duration** |
|
||||||
| **11** | **2025-01-XX** | **Added global mouse wheel zoom: `Ctrl+Wheel` on `document.body`, JSInterop callback to Blazor** |
|
| **11** | **2025-01-XX** | **Made zoom step configurable (buttons, Ctrl+Wheel, slider use same step)** |
|
||||||
| **11** | **2025-01-XX** | **Added PDF thumbnail sidebar (left panel) with page previews and navigation** |
|
| **11** | **2025-01-XX** | **Fixed thumbnail canvas alignment (object-fit: contain)** |
|
||||||
| **11** | **2025-01-XX** | **Implemented thumbnail rendering system with sequential loading (50ms delay between pages)** |
|
| **11** | **2025-01-XX** | **Fixed thumbnail re-rendering on sidebar toggle** |
|
||||||
| **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** |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user