# 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` 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:** Modern, high-performance PDF viewing without signing functionality. **Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings ### 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 `DotNetObjectReference` for callbacks - 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 = { qualityOptions, // Configurable from appsettings.json setQualityOptions(options), // Dynamic quality update initialize(canvasId, pdfDataUrl, dotNetRef), renderPage(num), renderThumbnail(pageNum, canvasId), attachWheelEvent(), // Ctrl+Wheel zoom (configurable step) zoomIn(), zoomOut(), // Configurable step percentage dispose() } ``` **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 1. **Unlimited Zoom:** - Canvas size not restricted by `max-width` - Frame stays fixed, scroll bars appear automatically - `text-align: center` for small sizes, full scroll for zoomed views 2. **Global Mouse Wheel Zoom:** - Event listener on `document.body` (works anywhere on page) - `Ctrl + Mouse Wheel` triggers `zoomIn()`/`zoomOut()` - Calls `dotNetReference.invokeMethodAsync('OnZoomChanged', scale)` to update Blazor UI - `{ passive: false }` to enable `preventDefault()` 3. **Render Task Cancellation:** - Stores `currentRenderTask` to cancel previous render if new one starts - Catches `RenderingCancelledException` to avoid console errors - Queue system (`pageNumPending`) for rapid page changes 4. **Thumbnail Sidebar:** - Left panel with page previews (sequential rendering, 50ms delay) - Click to navigate to specific page - Active page highlighted with gradient border - No header/title (maximizes thumbnail space) - Toggle button in toolbar to show/hide 5. **Resizable Splitter:** - 4px draggable divider between thumbnails and canvas - Min width: 150px, Max width: 400px - Visual feedback: gradient on hover/active - User preference saved to localStorage (`envelopeViewer_thumbnailWidth`) - Global mouse events (works anywhere during drag) - `col-resize` cursor (?) for intuitive UX 6. **Flex Layout:** - Thumbnails and canvas in same container (`.pdf-frame`) - `display: flex, flex-direction: row, align-items: stretch` - Perfect vertical alignment (same top/bottom position) - Responsive: column layout on mobile (<768px) 7. **Responsive Design:** - Desktop: 90% width, 1200px max - Mobile: 95% width, adjusted heights, thumbnails collapse to top - Adaptive padding and font sizes ### Initialization Flow ```csharp OnInitializedAsync(): 1. Fetch PDF bytes from DocumentService 2. Convert to base64 data URL 3. Set _isLoading = false 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) ``` **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 | 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`) 1. `AuthService.CheckEnvelopeAccessAsync` ? redirect to login if unauthorized 2. `AnnotationService.GetAnnotationsAsync` ? fills `_annotations` 3. `DocumentService.GetDocumentAsync` ? fills `_basePdfBytes` (real mode) 4. `BuildFreshBaseReport()` ? `XtraReport` for `DxReportViewer` ### Signature Popup - Tabs: Draw / Text / Image - Fields: full name (required), position (optional), place (required) - Saved to `_capturedSignature` record - If annotations exist ? popup closes ? JS checkbox overlays installed ### JS Checkbox Overlay (`receiver-signature.js`) - `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)` - One `.annot-sig-cb-wrapper` div per annotation, absolutely positioned over viewer scroll container - Position: `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY` - `scaleX = pagePixelWidth / 827`, `scaleY = pagePixelHeight / 1169` - Click ? `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)` ### Apply Signatures ("Unterschriften anwenden" — `SubmitSignaturesAsync`) **Real PDF mode (`_basePdfBytes` is set):** - Calls `StampSignaturesOnPdf` using **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 `XtraReport` with `XRPdfContent` - 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 ```csharp // 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 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() ```csharp // 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 | Session | |---|---|---| | `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 | | `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 | | `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 | | `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 | 10 | | **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** | | **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** | | **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** | | **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** | | **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** | | **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** | --- ## 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 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** | --- ## Layout Architecture (EnvelopeViewer) ### HTML Structure ```html
@if (_showThumbnails) {
}
``` ### CSS Flexbox Layout ```css .pdf-frame { display: flex; flex-direction: row; /* Side-by-side */ align-items: stretch; /* Same height */ overflow: hidden; } .pdf-thumbnails { flex-shrink: 0; /* Fixed width */ width: 260px; /* Dynamic via inline style */ border-right: none; /* Seamless join with splitter */ } .pdf-splitter { width: 4px; cursor: col-resize; flex-shrink: 0; } .pdf-canvas-wrapper { flex: 1; /* Fill remaining space */ overflow: auto; /* Scrollable for zoom */ padding: 2rem; text-align: center; } ``` ### Resizable Splitter Workflow ``` 1. User hovers splitter ? cursor: col-resize (?) 2. Mouse down ? OnSplitterMouseDown(e) - _isResizing = true - Store start position (clientX) and width - Add 'resizing' class to body (prevent text selection) - Call pdfViewer.startResize() 3. Mouse move (global) ? OnSplitterMouseMove(clientX) - Calculate delta = clientX - startX - newWidth = startWidth + delta - Clamp to 150-400px range - Update _thumbnailWidth - StateHasChanged() for reactive UI 4. Mouse up (global) ? OnSplitterMouseUp() - _isResizing = false - Remove 'resizing' class - Save to localStorage("envelopeViewer_thumbnailWidth") ``` ### Responsive Behavior - **Desktop (>768px)**: Flex row, side-by-side - **Mobile (?768px)**: Flex column, thumbnails on top - **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup