Files
EnvelopeGenerator/COPILOT_CONTEXT_EN.md
TekH c6d5656fce EnvelopeViewer updates and known issue documentation
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.
2026-06-06 00:03:01 +02:00

384 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `DotNetObjectReference` for callbacks
- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage
- CSS externalized to `envelope-viewer.css`
**JavaScript (`pdf-viewer.js`):**
```javascript
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
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. **Responsive Design:**
- Desktop: 90% width, 1200px max
- Mobile: 95% width, adjusted heights
- Adaptive padding and font sizes
### Flow
1. **Component Load:**
```csharp
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
```
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")`
3. **Cleanup:**
```csharp
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`)
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<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()
```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 |
|---|---|
| `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 |
|---|---|---|
| 13 | — | 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
- `OnAfterRenderAsync` triggers continuously in loop
**Root Cause:**
- `OnAfterRenderAsync` runs on every render cycle (not just `firstRender`)
- PDF canvas element is not in DOM when `pdfViewer.initialize` is called
- Because `_pdfLoaded = false`, thumbnail/toolbar sections don't render (`@if (_pdfLoaded)` condition)
- Each failed initialize triggers `StateHasChanged` ? new render ? `OnAfterRenderAsync` again ? **infinite loop**
**Attempted Fixes (Failed):**
1. **Adding `firstRender` check:**
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender && !_pdfLoaded && ...) {
// Initialize PDF
}
}
```
**Result:** Didn't stop the loop, still blank screen
2. **PDF.js availability check:**
```csharp
var pdfJsLoaded = await JSRuntime.InvokeAsync<bool>("eval", "typeof window.pdfjsLib !== 'undefined'");
```
**Result:** Didn't resolve canvas not found issue
3. **Increased delays:**
- `Task.Delay(300)` before initialize
- `Task.Delay(200)` before thumbnails
**Result:** No improvement
4. **JavaScript validation:**
- Added checks for `uint8Array.length`, `totalPages > 0`
**Result:** Didn't prevent initialization failure
**Possible Next Steps:**
1. **DOM Ready Strategy:**
- Wait for specific element existence before initialize
- Use `MutationObserver` in JS to detect canvas availability
- Try `IntersectionObserver` to ensure canvas is in viewport
2. **Conditional Rendering:**
- Always render canvas element (even before `_pdfLoaded`)
- Move toolbar/thumbnails outside `@if (_pdfLoaded)` block
- Use CSS `visibility: hidden` instead of conditional rendering
3. **Blazor Lifecycle:**
- Try `OnAfterRenderAsync` with `IJSRuntime` timeout guard
- Use `Task.Run` with cancellation token to prevent overlapping calls
- Investigate if WASM-specific render cycle differs from Server
4. **Debugging:**
- Add `Console.WriteLine` in C# to track render count
- Log `firstRender`, `_pdfLoaded`, `_pdfDataUrl` state on each call
- Check if PDF data is actually loaded (`_pdfDataUrl` not null/empty)
- Verify PDF.js CDN loads successfully (Network tab)
**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