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

17 KiB
Raw Blame History

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):

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:

    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:

    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

// 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
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:

    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:

    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