Documented the new client-side signature overlay system for EnvelopeViewer, replacing iText7 due to GPL license issues. Outlined the signature data structure in C# and JavaScript, and described the workflow steps for creating, applying, and displaying signatures as visual overlays. Added a "Future Enhancement Required" section suggesting commercial PDF libraries or server-side stamping for future improvements.
27 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. |
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
DotNetObjectReferencefor callbacks - Quality settings loaded from
appsettings.jsonviaIOptions<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
- HiDPI/Retina Support ? 4x quality on Retina displays
- Configurable Quality ? All parameters in appsettings.json
- Unlimited Zoom ? 50%-300%, configurable step (default 5%)
- Global Ctrl+Wheel Zoom ? Works anywhere on page
- Thumbnail Sidebar ? Resizable (150-400px), high-quality rendering
- Smooth Transitions ? Configurable fade effect
- Responsive Design ? Desktop/mobile adaptive layout
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
-
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
-
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-resizecursor (?) for intuitive UX
-
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)
- Thumbnails and canvas in same container (
-
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)
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 | 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 |
| 12 | 2025-01-XX | Fixed coordinate system documentation: Database stores INCHES (not DX units) |
| 12 | 2025-01-XX | Updated XML documentation in SignatureDto, AnnotationDto, AnnotationCreateDto |
| 13 | 2025-01-XX | Added SenderAppType enum to SignatureDto for Legacy/Blazor app differentiation |
| 13 | 2025-01-XX | Implemented signature overlay rendering in PDF.js viewer (INCHES ? normalized ? pixels) |
| 13 | 2025-01-XX | Added automatic overlay re-rendering on page change, zoom, and initial load |
| 13 | 2025-01-XX | Added clickable signature buttons on PDF canvas (Sign/Unterschreiben) |
| 13 | 2025-01-XX | Signature buttons: 150px×60px, purple gradient, pen icon, hover effects |
| 13 | 2025-01-XX | JavaScript: pdfViewer.renderSignatureButtons() and pdfViewer.clearSignatureButtons() |
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
- Initial Load: After PDF renders (
OnAfterRenderAsync) - Page Change:
NextPage(),PreviousPage(),GoToPageFromThumbnail() - 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)+ shadow0 4px 12px rgba(79, 70, 229, 0.4) - Active:
scale(0.98) - Focus:
2px solid #7e22ceoutline
- Hover:
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+)
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 (_capturedSignature):
record SignatureCapture(
string DataUrl, // base64 PNG: "data:image/png;base64,iVBORw0KG..."
string FullName, // Required: "Max Mustermann"
string Position, // Optional: "Geschäftsführer"
string Place // Required: "Berlin"
);
Applied Signature (per SignatureDto):
{
signatureId: 42,
dataUrl: "data:image/png;base64,iVBORw0KG...",
fullName: "Max Mustermann",
position: "Geschäftsführer",
place: "Berlin",
date: "15.01.2025",
x: 108, // PDF POINTS (already converted from INCHES)
y: 144, // PDF POINTS
width: 150, // pixels
height: 60 // pixels
}
Workflow Steps
Step 1: Signature Creation Popup
- Reuse
DxPopupfrom ReportViewer.razor - 3 tabs: Draw / Text / Image (using
receiver-signature.js) - Fields: Full name, Position (optional), Place
- Click "Speichern" ?
_capturedSignaturesaved to state
Step 2: Apply Signature to Canvas
- User clicks "Unterschreiben" button on PDF
- JavaScript creates HTML overlay (NOT Canvas drawing)
- Overlay repositions on zoom/page change
- NO PDF byte modification (GPL license issue)
Step 3: Visual Display Only
- Signatures shown as HTML
<div>overlays - Positioned using absolute positioning
- Persist in Blazor component state
- Lost on page refresh (no server-side save)
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