Introduced a new EnvelopeViewer component (`EnvelopeViewer.razor`) to replace the legacy ReportViewer for non-signing workflows. The new viewer is based on PDF.js and provides a modern, lightweight, and responsive user experience. Added `pdf-viewer.js` to handle PDF rendering, zoom, pagination, and mouse wheel control, integrated with Blazor via JSInterop. Externalized styles to `envelope-viewer.css` with a modern glassmorphism design. Enhanced user experience with unlimited zoom, global mouse wheel zoom (`Ctrl+Wheel`), and responsive design. Fixed issues like scroll behavior and canvas size restrictions. Updated `COPILOT_CONTEXT_EN.md` to document the new EnvelopeViewer, its architecture, and its advantages over the legacy ReportViewer. Added external dependencies for PDF.js via CDN and updated project history to reflect these changes.
13 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 |
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 | Updated COPILOT_CONTEXT_EN.md: EnvelopeViewer replaces ReportViewer for read-only viewing |