Files
EnvelopeGenerator/COPILOT_CONTEXT_EN.md
TekH ee3a142af0 Refactor signature model; update EnvelopeViewer UI
Refactored `_capturedSignature` to `SignatureCaptureDto`, a
sealed record with required and optional properties for better
type safety and clarity. Updated components to use the new
model and demonstrated initialization with object initializers.

Enhanced `EnvelopeViewer` with modern UI features, including
HiDPI/Retina support, configurable zoom options, smooth zoom
transitions, and improved thumbnail sidebar functionality.

Updated `Applied Signature` HTML overlay to support dynamic
positioning for rendering signatures at specific coordinates.

Revised documentation in `COPILOT_CONTEXT_EN.md` to reflect
changes in the signature data structure and usage. Noted that
current implementation provides visual overlays only, with
future consideration for actual PDF stamping using PSPDFKit.
2026-06-08 15:47:54 +02:00

32 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.

SignatureDto / AnnotationDto — Coordinate System

Database Storage Format: INCHES (GdPicture14 native unit)
Origin: Top-left corner of page
Axes: X increases rightward, Y increases downward

Source Evidence (VB.NET Legacy Code)

' From: EnvelopeGenerator.Form/frmFieldEditor.vb
' GdPicture14.Annotations.AnnotationStickyNote

'Breite und Höhe in Inches (4,5*5cm)
Private Const SIGNATURE_WIDTH As Single = 1.77   ' 1.77 inches = 4.5cm
Private Const SIGNATURE_HEIGHT As Single = 1.96  ' 1.96 inches = 5cm

Sub LoadAnnotation(pElement As Signature, ...)
    oAnnotation.Left = CSng(pElement.X)      ' Direct assignment ? INCHES
    oAnnotation.Top = CSng(pElement.Y)
    oAnnotation.Width = CSng(pElement.Width)
    oAnnotation.Height = CSng(pElement.Height)
End Sub

Conversion Formulas

Inches ? DevExpress (DX):  x_DX = x_inches * 100.0
                          y_DX = y_inches * 100.0

Inches ? PDF Points:       x_pt = x_inches * 72.0
                          y_pt = x_inches * 72.0

Inches ? PDF.js Canvas:    normalize to [0,1], then scale to pixels
                          x_norm = x_inches / pageWidth_inches
                          y_norm = y_inches / pageHeight_inches
                          x_px = x_norm * canvasWidth * scale * dpr
                          y_px = y_norm * canvasHeight * scale * dpr

Unit Comparison Table

System Unit Origin Conversion from INCHES
GdPicture14 (Source) Inches Top-left Database format (no conversion)
DevExpress (LEGACY) 1/100 inch (DX) Top-left x_DX = x_inches * 100
PDF.js (NEW) Pixels Top-left normalize ? scale
PDF Points (iText7) Points (1/72") Bottom-left x_pt = x_inches * 72 + Y-flip
PSPDFKit (Web) Points (1/72") Top-left x_pt = x_inches * 72

A4 Page Dimensions:

  • Width: 8.27 inches = 595 points = 827 DX units
  • Height: 11.69 inches = 842 points = 1169 DX units

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<PdfViewerOptions>
  • Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
  • CSS externalized to envelope-viewer.css

JavaScript (pdf-viewer.js):

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

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

{
  "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

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:

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

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


Layout Architecture (EnvelopeViewer)

HTML Structure

<div class="pdf-viewer-container">
  <div class="pdf-toolbar">
    <!-- Zoom, page navigation, thumbnail toggle -->
  </div>
  
  <div class="pdf-frame">
    @if (_showThumbnails) {
      <div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
        <!-- Page previews -->
      </div>
      <div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
        <!-- Resizable divider -->
      </div>
    }
    <div class="pdf-canvas-wrapper">
      <canvas id="pdf-canvas" class="pdf-canvas"></canvas>
    </div>
  </div>
</div>

CSS Flexbox Layout

.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

Signature Buttons in EnvelopeViewer — Interactive Overlay System

Purpose: Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.

Architecture

Blazor Component (EnvelopeViewer.razor):

IReadOnlyList<SignatureDto> _signatures = [];

protected override async Task OnInitializedAsync() {
    var signatures = await SignatureService.GetAsync(EnvelopeKey);
    _signatures = signatures.Convert(UnitOfLength.Point);  // INCHES ? POINTS
}

async Task RenderSignatureButtonsAsync() {
    await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", 
        _signatures, _currentPage, _dotNetRef);
}

[JSInvokable]
public void OnSignatureButtonClick(int signatureId) {
    Console.WriteLine($"Signature #{signatureId} signed");
}

JavaScript (pdf-viewer.js):

renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
    this.clearSignatureButtons();  // Remove old buttons
    
    const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
    const signatureLayer = document.getElementById('pdf-signature-layer');
    
    pageSignatures.forEach(sig => {
        // Convert POINTS to display pixels
        const xPx = sig.x * this.scale;  // sig.x already in PDF POINTS
        const yPx = sig.y * this.scale;
        
        const button = document.createElement('button');
        button.className = 'signature-button';
        button.style.left = `${xPx}px`;
        button.style.top = `${yPx}px`;
        button.style.width = '150px';
        button.style.height = '60px';
        
        // German text + pen icon
        button.innerHTML = `
            <div>Unterschreiben</div>
            <svg>...</svg>
        `;
        
        button.addEventListener('click', () => {
            this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
        });
        
        signatureLayer.appendChild(button);
        this.signatureButtons.push(button);
    });
}

clearSignatureButtons() {
    this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
    this.signatureButtons = [];
}

HTML Structure:

<div class="pdf-page-container">
    <canvas id="pdf-canvas"></canvas>
    <div id="pdf-text-layer"></div>
    <div id="pdf-signature-layer"></div>  <!-- NEW -->
</div>

CSS (envelope-viewer.css):

.pdf-signature-layer {
    position: absolute;
    left: 0; top: 0; right: 0; bottom: 0;
    overflow: visible;
    pointer-events: none;  /* Pass clicks through to canvas */
    z-index: 20;
}

.signature-button {
    pointer-events: auto;  /* Re-enable for buttons */
    position: absolute;
    width: 150px; height: 60px;
    background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    /* Hover: scale(1.05), shadow, darker gradient */
}

Rendering Triggers

  1. Initial Load: After PDF renders (OnAfterRenderAsync)
  2. Page Change: NextPage(), PreviousPage(), GoToPageFromThumbnail()
  3. Zoom Change: OnZoomChanged() (re-calculates pixel positions)

Coordinate Conversion Flow

Database (INCHES)
  ? SignatureService.GetAsync()
SignatureDto.X/Y (INCHES)
  ? .Convert(UnitOfLength.Point)
SignatureDto.X/Y (PDF POINTS, × 72)
  ? JavaScript
Display Pixels (× this.scale)
  ? CSS
button.style.left/top

Example Calculation:

  • Database: X = 1.5 inches, Y = 2.0 inches
  • After conversion: X = 108 points, Y = 144 points (× 72)
  • At scale 1.5: X = 162px, Y = 216px
  • Button positioned at left: 162px, top: 216px

Button Design

Visual Spec:

  • Size: 150px × 60px
  • Background: Purple gradient #4F46E5 ? #4338CA (hover: darker)
  • Text: "Unterschreiben" (18px, bold, white)
  • Icon: Pen SVG (24px white)
  • Effects:
    • Hover: scale(1.05) + shadow 0 4px 12px rgba(79, 70, 229, 0.4)
    • Active: scale(0.98)
    • Focus: 2px solid #7e22ce outline

Accessibility:

  • tabindex="0" for keyboard navigation
  • Focus outline for keyboard users
  • Click handler with semantic button element

Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14)

IMPORTANT: iText7 NOT USED in EnvelopeViewer

  • Reason: GPL license incompatibility (requires source code sharing)
  • Alternative: Client-side signature overlay system (HTML + Canvas API)
  • Export: Signatures are visual overlays only, NOT stamped on PDF bytes
  • Future: Consider PSPDFKit or commercial PDF library for actual PDF stamping

Signature Data Structure

Captured Signature (SignatureCaptureDto):

// Model: EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
public sealed record SignatureCaptureDto
{
    public required string DataUrl { get; init; }      // base64 PNG: "data:image/png;base64,iVBORw0KG..."
    public required string FullName { get; init; }     // Required: "Max Mustermann"
    public string Position { get; init; } = string.Empty;  // Optional: "Geschäftsführer"
    public required string Place { get; init; }        // Required: "Berlin"
}

// Usage in components:
SignatureCaptureDto? _capturedSignature;

// Initialization with object initializer (required properties):
_capturedSignature = new SignatureCaptureDto
{
    DataUrl = signatureDataUrl,
    FullName = _signerFullName.Trim(),
    Position = _signerPosition.Trim(),
    Place = _signaturePlace.Trim()
};

Applied Signature (HTML Overlay):

<div class="applied-signature" data-signature-id="42" style="left: 162px; top: 216px;">
    <img src="data:image/png;base64,..." />  <!-- Signature image (max 70px height) -->
    <div style="border-top: 1px solid #495057;"></div>  <!-- Separator line -->
    <div style="font-size: 9px; color: #495057;">
        <strong>Max Mustermann</strong>     <!-- Name (bold, #212529) -->
        <br>Geschäftsführer                <!-- Position (optional) -->
        <br>Berlin, 26.01.2025              <!-- Place, Date (dd.MM.yyyy) -->
    </div>
</div>

Complete Workflow (Session 14 Update)

Step 1: Page Load & Automatic Popup

protected override async Task OnInitializedAsync() {
    // ... load PDF and signatures ...
    
    // Open signature popup automatically
    _activeSignatureTab = SignatureTabDraw;
    _signaturePopupVisible = true;
    _popupValidationMessage = null;
}

Features:

  • Opens automatically on page load (no manual trigger needed)
  • Cannot be closed manually (no X button, ESC disabled, no outside-click)
  • User MUST create signature before viewing PDF

Step 2: Signature Creation Popup (DxPopup)

Tabs:

  1. Zeichnen (Draw): Canvas-based signature pad (receiver-signature.js)

    • Touch-friendly: touch-action: none
    • Line width: 2.5px, black (#111)
    • Canvas: 560×180px, rounded corners, shadow
  2. Text: Type signature with font selection

    • Fonts: Brush Script, Segoe Script, Lucida Handwriting, Comic Sans, Cursive
    • Real-time preview on canvas
  3. Bild (Image): Upload PNG/JPG/WebP

    • File input with preview
    • Auto-resize to fit canvas

Required Fields:

  • ? Vor- und Nachname (Full Name) — Red asterisk *
  • ? Ort (Place) — Red asterisk *
  • ? Position — Optional, gray "(optional)" label

Validation:

async Task SaveSignatureAsync() {
    if (string.IsNullOrWhiteSpace(_signerFullName)) {
        _popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
        return;
    }
    if (string.IsNullOrWhiteSpace(_signaturePlace)) {
        _popupValidationMessage = "Bitte geben Sie den Ort ein.";
        return;
    }
    var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
    if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
        _popupValidationMessage = "Die Unterschrift ist erforderlich.";
        return;
    }
    
    // Save to session state
    _capturedSignature = new(signatureDataUrl, _signerFullName.Trim(), 
                            _signerPosition.Trim(), _signaturePlace.Trim());
    _signaturePopupVisible = false;
}

Design (Modern & Clean):

  • Tabs: Purple active state (#4F46E5), 3px bottom border
  • Inputs: 2px solid border (#e9ecef), 6px border-radius, consistent padding
  • Canvas: Light shadow (0 1px 3px rgba(0,0,0,0.1)), rounded corners
  • Buttons:
    • Erneuern: Outline secondary with refresh icon
    • Speichern: Purple gradient + checkmark icon + shadow
  • Error: Red left border (4px solid #dc3545), light red background (#fee)

Step 3: PDF Viewing with Signature Buttons

After popup closes:

protected override async Task OnAfterRenderAsync(bool firstRender) {
    // ... PDF initialization ...
    
    await RenderSignatureButtonsAsync();  // Render "Unterschreiben" buttons
}

Buttons appear at signature field positions:

  • Purple gradient background
  • "Unterschreiben" text + pen icon
  • Hover: scale(1.05) + darker color
  • Positioned using PDF POINTS ? display pixels conversion

Step 4: Apply Signature (Click "Unterschreiben")

C# Handler:

[JSInvokable]
public async Task OnSignatureButtonClick(int signatureId) {
    if (_capturedSignature == null) return;
    
    await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature", 
        signatureId, 
        _capturedSignature.DataUrl,
        _capturedSignature.FullName,
        _capturedSignature.Position,
        _capturedSignature.Place);
}

JavaScript Implementation:

async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
    // 1. Find and remove button
    const button = this.signatureButtons.find(btn => 
        btn.getAttribute('data-signature-id') == signatureId);
    button.parentNode.removeChild(button);
    
    // 2. Create signature container (German standard format)
    const signatureContainer = document.createElement('div');
    signatureContainer.className = 'applied-signature';
    signatureContainer.style.position = 'absolute';
    signatureContainer.style.left = button.style.left;  // Same position as button
    signatureContainer.style.top = button.style.top;
    signatureContainer.style.width = '230px';
    signatureContainer.style.backgroundColor = '#f8f9fa';
    signatureContainer.style.border = '1px solid #dee2e6';
    signatureContainer.style.borderRadius = '6px';
    signatureContainer.style.padding = '12px';
    
    // 3. Add signature image
    const img = document.createElement('img');
    img.src = signatureDataUrl;
    img.style.width = '100%';
    img.style.maxHeight = '70px';
    img.style.objectFit = 'contain';
    
    // 4. Add separator line (German standard)
    const separator = document.createElement('div');
    separator.style.borderTop = '1px solid #495057';
    separator.style.marginTop = '6px';
    separator.style.marginBottom = '8px';
    
    // 5. Add text information
    const today = new Date();
    const dateStr = today.toLocaleDateString('de-DE', { 
        day: '2-digit', month: '2-digit', year: 'numeric' 
    });  // "26.01.2025"
    
    const infoHtml = [
        `<strong>${this.escapeHtml(fullName)}</strong>`,
        position ? this.escapeHtml(position) : null,
        `${this.escapeHtml(place)}, ${dateStr}`
    ].filter(x => x).join('<br>');
    
    const info = document.createElement('div');
    info.style.fontSize = '9px';
    info.style.color = '#495057';
    info.innerHTML = infoHtml;
    
    // 6. Assemble and add to layer
    signatureContainer.appendChild(img);
    signatureContainer.appendChild(separator);
    signatureContainer.appendChild(info);
    
    document.getElementById('pdf-signature-layer').appendChild(signatureContainer);
}

German Standard Layout:

???????????????????????????????
?  [Signature Image]          ?  ? Base64 PNG, max 70px height
?                             ?
???????????????????????????????  ? 1px separator (#495057)
?                             ?
?  Max Mustermann (Bold)      ?  ? Name (font-weight: 600, #212529)
?  Geschäftsführer            ?  ? Position (optional, normal weight)
?  Berlin, 26.01.2025         ?  ? Place, Date (dd.MM.yyyy)
?                             ?
???????????????????????????????

Step 5: Persistence & Re-rendering

  • Zoom/Page Change: Applied signatures re-render automatically
  • Session State: _capturedSignature stored in Blazor component
  • Limitation: Lost on page refresh (no server-side storage)
  • Future: Export to PDF with actual byte stamping (requires non-GPL library)

Security:

escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;  // Browser auto-escapes
    return div.innerHTML;
}

Protects against XSS attacks from malicious input in Name/Position/Place fields.


Popup Design Specification

DxPopup Properties:

<DxPopup @bind-Visible="_signaturePopupVisible"
         HeaderText="Unterschrift erstellen"
         Width="620px"
         MaxWidth="95vw"
         ShowFooter="true"       <!-- REQUIRED for buttons to appear -->
         CloseOnOutsideClick="false"
         ShowCloseButton="false"  <!-- No X button -->
         CloseOnEscape="false">  <!-- ESC disabled -->

Tab Design:

  • Active Tab: border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;
  • Inactive Tab: color: #6c757d;
  • Tab Bar: border-bottom: 2px solid #e9ecef;

Input Styling:

input, select {
    border: 2px solid #e9ecef;
    border-radius: 6px;
    padding: 0.625rem;
}

Canvas Styling:

canvas {
    border: 2px solid #e9ecef;
    border-radius: 8px;
    background: white;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

Button Styling:

/* Erneuern (Renew) */
.btn-outline-secondary {
    border-radius: 6px;
    padding: 0.625rem 1.25rem;
    font-weight: 500;
}

/* Speichern (Save) */
.btn-primary {
    background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
    border: none;
    border-radius: 6px;
    padding: 0.625rem 2rem;
    font-weight: 600;
    box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);
}

Future Enhancement Required:

  • Replace iText7 with commercial PDF library (e.g., PSPDFKit, Syncfusion)
  • Or use server-side stamping with Azure PDF Services
  • Or accept GPL license and open-source the stamping code