Add signature overlay workflow to PDF.js viewer

Implemented a complete client-side workflow for creating, applying, and displaying visual signature overlays on the PDF canvas.

- Added "Unterschreiben" buttons with modern styling (purple gradient, hover effects) to apply signatures.
- Introduced a signature creation popup (DxPopup) with Draw, Text, and Image tabs, including validation for required fields (Name, Place).
- Rendered German-style signature overlays with image, separator line, and text (Name, Position, Place, Date in dd.MM.yyyy format).
- Ensured automatic re-rendering of overlays on page load, zoom, and page changes.
- Added `escapeHtml()` for XSS protection of user-provided text.
- Styled popup, canvas, and buttons with a modern, clean design.
- Suggested future enhancements for server-side stamping or integration with commercial PDF libraries.
This commit is contained in:
2026-06-07 14:08:38 +02:00
parent ce43ace3c2
commit 3f52858fe9

View File

@@ -427,6 +427,16 @@ Our use case is **visual/image stamping** at specific page coordinates
| **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()`** |
| **14** | **2025-01-26** | **Added signature creation popup (DxPopup) - opens automatically on page load** |
| **14** | **2025-01-26** | **Popup features: 3 tabs (Draw/Text/Image), required fields (Name, Place), optional Position** |
| **14** | **2025-01-26** | **Popup: No close button (X), no ESC/outside-click - only saves with validation** |
| **14** | **2025-01-26** | **Modern, clean popup design matching EnvelopeViewer theme (purple gradients, rounded inputs)** |
| **14** | **2025-01-26** | **Implemented German-style professional signature rendering on PDF canvas** |
| **14** | **2025-01-26** | **Click "Unterschreiben" button ? removes button, renders applied signature** |
| **14** | **2025-01-26** | **Signature layout: Image + separator line + Name (bold) + Position + Place, Date** |
| **14** | **2025-01-26** | **JavaScript: `pdfViewer.applySignature()` creates HTML overlay (230px box, #f8f9fa background)** |
| **14** | **2025-01-26** | **German date format: dd.MM.yyyy (e.g., 26.01.2025) via `toLocaleDateString('de-DE')`** |
| **14** | **2025-01-26** | **XSS protection: `escapeHtml()` function sanitizes user-provided text** |
---
@@ -657,7 +667,7 @@ button.style.left/top
---
## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13+)
## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14)
**IMPORTANT: iText7 NOT USED in EnvelopeViewer**
- **Reason:** GPL license incompatibility (requires source code sharing)
@@ -672,46 +682,280 @@ button.style.left/top
record SignatureCapture(
string DataUrl, // base64 PNG: "data:image/png;base64,iVBORw0KG..."
string FullName, // Required: "Max Mustermann"
string Position, // Optional: "Geschäftsführer"
string Position, // Optional: "Geschäftsführer" (can be empty)
string Place // Required: "Berlin"
);
```
**Applied Signature (per SignatureDto):**
```javascript
{
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
**Applied Signature (HTML Overlay):**
```html
<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**
```csharp
protected override async Task OnInitializedAsync() {
// ... load PDF and signatures ...
// Open signature popup automatically
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = true;
_popupValidationMessage = null;
}
```
### Workflow Steps
**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 1: Signature Creation Popup**
- Reuse `DxPopup` from ReportViewer.razor
- 3 tabs: Draw / Text / Image (using `receiver-signature.js`)
- Fields: Full name, Position (optional), Place
- Click "Speichern" ? `_capturedSignature` saved to state
**Step 2: Signature Creation Popup (DxPopup)**
**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)
**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
**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)
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:**
```csharp
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:
```csharp
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:**
```csharp
[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:**
```javascript
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:**
```javascript
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:**
```razor
<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:**
```css
input, select {
border: 2px solid #e9ecef;
border-radius: 6px;
padding: 0.625rem;
}
```
**Canvas Styling:**
```css
canvas {
border: 2px solid #e9ecef;
border-radius: 8px;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
```
**Button Styling:**
```css
/* 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)