diff --git a/COPILOT_CONTEXT_EN.md b/COPILOT_CONTEXT_EN.md index 342d8ebb..86883058 100644 --- a/COPILOT_CONTEXT_EN.md +++ b/COPILOT_CONTEXT_EN.md @@ -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 +
+ +
+
+ Max Mustermann +
Geschäftsführer +
Berlin, 26.01.2025 +
+
+``` + +### 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 `
` 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 = [ + `${this.escapeHtml(fullName)}`, + position ? this.escapeHtml(position) : null, + `${this.escapeHtml(place)}, ${dateStr}` + ].filter(x => x).join('
'); + + 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 + + CloseOnOutsideClick="false" + ShowCloseButton="false" + CloseOnEscape="false"> +``` + +**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)