Updated EnvelopeViewer with layout fixes, unlimited zoom, and thumbnail navigation. Added global mouse wheel zoom (`Ctrl+Wheel`) and retry logic for thumbnail rendering. Refactored layout for responsiveness and documented a critical issue causing a blank screen and infinite render loop. Proposed next steps for resolution and provided a temporary workaround using the legacy ReportViewer.
17 KiB
EnvelopeGenerator — Copilot Context Notes (English)
Purpose
A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF.
Solution Structure
| Project | Target | Description |
|---|---|---|
EnvelopeGenerator.API |
net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. |
EnvelopeGenerator.ReceiverUI |
net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. |
EnvelopeGenerator.Web |
net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. |
EnvelopeGenerator.Application |
multi | MediatR CQRS handlers. |
EnvelopeGenerator.Domain |
multi | Domain models, constants, interfaces. |
EnvelopeGenerator.Infrastructure |
multi | EF Core repos, DB context. |
EnvelopeGenerator.PdfEditor |
multi | iText7 utilities. NOT used in ReceiverUI flow. |
EnvelopeGenerator.DependencyInjection |
multi | DI registration helpers. |
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
Key Files
| File | Purpose |
|---|---|
ReceiverUI/Pages/EnvelopeViewer.razor |
NEW PDF.js-based viewer (/envelope/{key}). Replaces ReportViewer.razor. Simple read-only PDF viewing with zoom/navigation. |
ReceiverUI/Pages/ReportViewer.razor |
LEGACY DevExpress-based signing page (/receiver/{key}). Still used for signature workflow. Will be deprecated. |
ReceiverUI/wwwroot/js/pdf-viewer.js |
NEW PDF.js wrapper: rendering, zoom, pagination, mouse wheel control. |
ReceiverUI/wwwroot/js/receiver-signature.js |
JS: checkbox overlay, signature pad (draw/type/image). |
ReceiverUI/wwwroot/css/envelope-viewer.css |
NEW Styles for EnvelopeViewer.razor (external CSS, not inline). |
ReceiverUI/wwwroot/fake-data/annotations.json |
Dev-mode fake annotations (YARP proxy target). |
ReceiverUI/Models/AnnotationDto.cs |
Annotation position model. All properties non-nullable. |
ReceiverUI/Services/AnnotationService.cs |
Fetches List<AnnotationDto> from API or fake-data. |
ReceiverUI/Services/DocumentService.cs |
Fetches PDF bytes from API. |
ReceiverUI/Services/AuthService.cs |
Manages receiver session cookie. |
API/Controllers/AnnotationController.cs |
GET api/Annotation/{key} ? annotation list. |
API/Controllers/DocumentController.cs |
GET api/Document/{key} ? PDF bytes. |
AnnotationDto — Coordinate System
Unit : 1/100 inch (DX units) — DevExpress XtraReports native
Origin : Top-left corner of page
X : increases rightward
Y : increases downward
A4 in DX units: Width = 827, Height = 1169
Conversions:
PSPDFKit (pt, top-left): xDX = xPsPdf * (100/72)
GDPicture (pt, bottom-left): yDX = (pageHeightPt - yGD - elemHeightPt) * (100/72)
DX ? PDF points: pt = dx * (72/100)
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
Architecture
Blazor Component (EnvelopeViewer.razor):
- Fetches PDF via
DocumentService.GetDocumentAsync(EnvelopeKey) - Converts to base64 data URL:
data:application/pdf;base64,{base64} - Initializes PDF.js viewer via JSInterop with
DotNetObjectReferencefor callbacks - Displays controls: Zoom In/Out, Page Navigation, Zoom percentage
- CSS externalized to
envelope-viewer.css
JavaScript (pdf-viewer.js):
window.pdfViewer = {
pdfDoc, canvas, ctx, scale, currentRenderTask,
dotNetReference, wheelEventAttached,
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
attachWheelEvent(), // Global Ctrl+Wheel zoom
zoomIn(), zoomOut(),
nextPage(), previousPage(),
dispose()
}
CSS (envelope-viewer.css):
.envelope-viewer-layout: Full-height gradient background.envelope-action-bar: Top bar with logo, title, controls (sticky).pdf-frame: Fixed-size white container (calc(100vh - 200px)× 90% width, max 1200px).pdf-canvas:display: inline-block, unlimited zoom, scrollable when exceeds frame- Modern glassmorphism design with gradients and shadows
Features
-
Unlimited Zoom:
- Canvas size not restricted by
max-width - Frame stays fixed, scroll bars appear automatically
text-align: centerfor small sizes, full scroll for zoomed views
- Canvas size not restricted by
-
Global Mouse Wheel Zoom:
- Event listener on
document.body(works anywhere on page) Ctrl + Mouse WheeltriggerszoomIn()/zoomOut()- Calls
dotNetReference.invokeMethodAsync('OnZoomChanged', scale)to update Blazor UI { passive: false }to enablepreventDefault()
- Event listener on
-
Render Task Cancellation:
- Stores
currentRenderTaskto cancel previous render if new one starts - Catches
RenderingCancelledExceptionto avoid console errors - Queue system (
pageNumPending) for rapid page changes
- Stores
-
Responsive Design:
- Desktop: 90% width, 1200px max
- Mobile: 95% width, adjusted heights
- Adaptive padding and font sizes
Flow
-
Component Load:
OnInitializedAsync(): - Fetch PDF bytes - Convert to base64 data URL - Set _isLoading = false OnAfterRenderAsync(): - Create DotNetObjectReference - JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef) - Update _totalPages, _currentPage, _pdfLoaded -
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")
- Button clicks ?
-
Cleanup:
DisposeAsync(): - JSRuntime.InvokeVoidAsync("pdfViewer.dispose") - _dotNetRef?.Dispose()
Key Differences from ReportViewer
| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) |
|---|---|---|
| Technology | PDF.js + Canvas | DevExpress XtraReports |
| Route | /envelope/{key} |
/receiver/{key} |
| Purpose | Read-only viewing | Signature workflow |
| Dependencies | PDF.js CDN | DevExpress NuGet packages |
| Zoom | Unlimited, smooth | Report viewer default |
| Mouse Wheel | Custom Ctrl+Wheel | Browser default |
| File Size | Minimal (JS + CSS) | Heavy (DX libs) |
| Maintenance | Simple, standard web | Complex, vendor-specific |
ReceiverUI Signing Flow (ReportViewer.razor — LEGACY)
On Load (OnInitializedAsync)
AuthService.CheckEnvelopeAccessAsync? redirect to login if unauthorizedAnnotationService.GetAnnotationsAsync? fills_annotationsDocumentService.GetDocumentAsync? fills_basePdfBytes(real mode)BuildFreshBaseReport()?XtraReportforDxReportViewer
Signature Popup
- Tabs: Draw / Text / Image
- Fields: full name (required), position (optional), place (required)
- Saved to
_capturedSignaturerecord - If annotations exist ? popup closes ? JS checkbox overlays installed
JS Checkbox Overlay (receiver-signature.js)
receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)- One
.annot-sig-cb-wrapperdiv per annotation, absolutely positioned over viewer scroll container - Position:
left = pageRect.left + ann.x * scaleX,top = pageRect.top + ann.y * scaleYscaleX = pagePixelWidth / 827,scaleY = pagePixelHeight / 1169
- Click ?
dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)
Apply Signatures ("Unterschriften anwenden" — SubmitSignaturesAsync)
Real PDF mode (_basePdfBytes is set):
- Calls
StampSignaturesOnPdfusing iText7 directly on PDF bytes - Coordinate conversion:
xPt = ann.X * (72/100),yPt = pageHeight - ann.Y * (72/100) - sigHeight(Y flip: DX top-down ? PDF bottom-up) - Returns stamped bytes ? loaded into new
XtraReportwithXRPdfContent - Viewer refreshed with
ViewerKey++
Dev/fake mode (_basePdfBytes is null):
- Falls back to
AddSignatureAtAnnotation(XtraReports DetailBand + BeforePrint counter) - This is intentionally left as a dev fallback only
Export
reportViewer.ExportToAsync(ExportFormat.Pdf)
StampSignaturesOnPdf — iText7 Implementation
// Located in ReportViewer.razor @code section
// Called by SubmitSignaturesAsync when _basePdfBytes is available
static byte[] StampSignaturesOnPdf(
byte[] sourcePdfBytes, byte[] signatureImageBytes,
string signerFullName, string signerPosition, string signaturePlace,
IReadOnlyList<AnnotationDto> annotations)
{
// Opens PDF with PdfReader/PdfWriter
// For each annotation:
// pageNum = ann.Page (clamped to totalPages)
// xPt = ann.X * (72f/100f)
// imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip
// PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false)
// Separator line at bottom of image
// Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87))
// Returns stamped byte[]
}
BuildFreshBaseReport()
// Real PDF mode:
var report = new XtraReport();
var detail = new DetailBand();
report.Bands.Add(detail);
detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
return report;
// Dev/fake mode: returns pre-built report from ReportStorage
NuGet Packages in ReceiverUI
| Package | Version | Purpose |
|---|---|---|
DevExpress.Blazor.Reporting.Viewer |
25.2.3 | DxReportViewer (LEGACY, used in ReportViewer.razor) |
DevExpress.Blazor.PdfViewer |
25.2.3 | PDF viewer (not used in EnvelopeViewer) |
DevExpress.Drawing.Skia |
25.2.3 | Drawing backend |
itext |
8.0.5 | PDF stamping (iText7) |
SkiaSharp.* |
3.119.1 | WASM native rendering |
External CDN (EnvelopeViewer):
- PDF.js 3.11.174 (via
https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js) - PDF.js Worker (
pdf.worker.min.js)
Mistakes History — Do NOT Repeat
| Mistake | Why Wrong |
|---|---|
BottomMarginBand for per-page signatures |
Repeats on every page; Y offset wrong |
imageY = (page-1) * 1169 + ann.Y |
Inflates DetailBand; 35 pages ? 140 pages |
e.Graph?.PrintingSystem in BeforePrint |
Graph not on CancelEventArgs |
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 |
DevExpress Article (2023-08-28) — Why It Does NOT Apply
The article describes X.509 cryptographic digital signatures via PdfDocumentSigner + Pkcs7Signer.
Our use case is visual/image stamping at specific page coordinates — different problem, different API.
XRPdfSignature in the article requires pre-placed fields in the report designer, not runtime coordinates.
Change Log
| Session | Date | Change |
|---|---|---|
| 1–3 | — | Core infrastructure: services, YARP proxy, JS overlay, signature pad |
| 4 | — | AddSignatureAtAnnotation with BottomMarginBand — ? repeated on all pages |
| 5 | — | BeforePrint + e.Graph?.PrintingSystem — ? compile error |
| 6 | — | BeforePrint counter — ? correct pattern, wrong band |
| 7 | — | Switched to DetailBand — ? correct band |
| 8 | — | (page-1)*1169+Y offset — ? 35?140 page inflation |
| 9 | — | Fixed: BoundsF.Y = ann.Y + counter; created COPILOT_CONTEXT.md |
| 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: Side-by-side flex design (thumbnails left, PDF right), responsive mobile (horizontal scroll thumbnails) |
| 11 | 2025-01-XX | Updated COPILOT_CONTEXT_EN.md: EnvelopeViewer replaces ReportViewer for read-only viewing |
| 11 | 2025-01-XX | ?? UNRESOLVED: Infinite render loop causing blank screen — Canvas not found error repeating, _pdfLoaded never becomes true |
Known Issues
EnvelopeViewer — Blank Screen / Infinite Loop (UNRESOLVED)
Symptoms:
- Browser console shows:
Canvas not found: pdf-canvas(repeating 20+ times) - UI displays error: "Fehler beim Laden des Dokuments - PDF konnte nicht initialisiert werden"
- Blank purple gradient screen, no PDF or thumbnails visible
OnAfterRenderAsynctriggers continuously in loop
Root Cause:
OnAfterRenderAsyncruns on every render cycle (not justfirstRender)- PDF canvas element is not in DOM when
pdfViewer.initializeis called - Because
_pdfLoaded = false, thumbnail/toolbar sections don't render (@if (_pdfLoaded)condition) - Each failed initialize triggers
StateHasChanged? new render ?OnAfterRenderAsyncagain ? infinite loop
Attempted Fixes (Failed):
-
Adding
firstRendercheck:protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && !_pdfLoaded && ...) { // Initialize PDF } }Result: Didn't stop the loop, still blank screen
-
PDF.js availability check:
var pdfJsLoaded = await JSRuntime.InvokeAsync<bool>("eval", "typeof window.pdfjsLib !== 'undefined'");Result: Didn't resolve canvas not found issue
-
Increased delays:
Task.Delay(300)before initializeTask.Delay(200)before thumbnails Result: No improvement
-
JavaScript validation:
- Added checks for
uint8Array.length,totalPages > 0Result: Didn't prevent initialization failure
- Added checks for
Possible Next Steps:
-
DOM Ready Strategy:
- Wait for specific element existence before initialize
- Use
MutationObserverin JS to detect canvas availability - Try
IntersectionObserverto ensure canvas is in viewport
-
Conditional Rendering:
- Always render canvas element (even before
_pdfLoaded) - Move toolbar/thumbnails outside
@if (_pdfLoaded)block - Use CSS
visibility: hiddeninstead of conditional rendering
- Always render canvas element (even before
-
Blazor Lifecycle:
- Try
OnAfterRenderAsyncwithIJSRuntimetimeout guard - Use
Task.Runwith cancellation token to prevent overlapping calls - Investigate if WASM-specific render cycle differs from Server
- Try
-
Debugging:
- Add
Console.WriteLinein C# to track render count - Log
firstRender,_pdfLoaded,_pdfDataUrlstate on each call - Check if PDF data is actually loaded (
_pdfDataUrlnot null/empty) - Verify PDF.js CDN loads successfully (Network tab)
- Add
Test Case:
- Route:
/envelope/NTE3Ym15SyUtNjA4M...(valid envelope key) - Expected: PDF loads, thumbnails appear, toolbar shows
- Actual: Blank screen, console error spam, no PDF rendering
Workaround:
- Use legacy
ReportViewer.razor(/receiver/{key}) for now - EnvelopeViewer development paused until root cause identified