From 2a9bbb3fe56a9e8048827e05db0d88ad5b2e49bf Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 1 Jul 2026 11:25:04 +0200 Subject: [PATCH 1/2] Add envelope editor with PDF upload and signature tools Introduced a new Blazor page `EnvelopeSenderEditorPage.razor` for editing envelopes with an interactive interface. Integrated `DxPdfViewer` for rendering PDFs and added functionality for uploading, viewing, and interacting with PDF files. Key features: - Action bar with buttons for uploading PDFs, toggling signature placement mode, clearing fields, and saving. - Placement mode for adding signature fields via an overlay, with visual placeholders. - JavaScript interop (`envelope-editor.js`) for precise click coordinate mapping. - Error handling for unsupported file types and size limits (50 MB). - Logging for debugging key actions like PDF uploads and field placements. Defined constants for accurate signature field dimensions and scaling. Added models (`SignatureFieldDraft`, `OverlayCoords`) to manage state and interactions. --- .../Pages/EnvelopeSenderEditorPage.razor | 343 ++++++++++++++++++ .../wwwroot/js/envelope-editor.js | 21 ++ 2 files changed, 364 insertions(+) create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor new file mode 100644 index 00000000..fbc04868 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor @@ -0,0 +1,343 @@ +@page "/envelope/editor" +@rendermode InteractiveServer +@using DevExpress.Blazor.PdfViewer +@using DevExpress.Blazor.Reporting.Models +@using DevExpress.Blazor +@using EnvelopeGenerator.Server.Client.Services +@using Microsoft.AspNetCore.Components.Forms +@inject IJSRuntime JSRuntime +@inject AppVersionService AppVersion +@inject ILogger Logger + + + + + +
+ + @* ── Action Bar ── *@ +
+
+ + @* Left: Title *@ +
+
+ Neues Dokument +
+ @if (_pdfLoaded) + { + @_fileName + @if (_signatureFields.Count > 0) + { + + @_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "") + + } + } +
+ + @* Right: Buttons *@ +
+ + @* PDF Upload *@ + + + @if (_pdfLoaded) + { + @* Toggle placement mode *@ + + + @* Clear all fields *@ + @if (_signatureFields.Count > 0) + { + + } + + @* Save *@ + + } +
+
+ + @* Placement mode hint bar *@ + @if (_placementMode) + { +
+ 📌 Klicken Sie auf die gewünschte Stelle im Dokument, um ein Signaturfeld zu platzieren. +
+ } +
+ + @* ── Content ── *@ +
+ @if (!_pdfLoaded) + { + @* Empty state *@ +
+
+
+ + + +
+
Kein Dokument geladen
+

+ Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren. +

+ +
+
+ } + else if (_errorMessage is not null) + { +
+
+ Fehler: @_errorMessage +
+
+ } + else + { + @* PDF viewer + overlay wrapper *@ +
+ + @* DxPdfViewer — zoom fixed to 1.0 for reliable coordinate mapping *@ + + + @* Transparent overlay for click capture (active only in placement mode) *@ +
+ + @* Render placed signature field placeholders *@ + @foreach (var field in _signatureFields) + { + var f = field; // capture for lambda +
+ + Unterschrift + + + S.@f.Page + + +
+ } +
+
+ } +
+
+ +@code { + // ── Constants ── + // Signature field size in PDF points (fixed): 1.77" × 1.96" × 72 pt/inch + const double SigWidthPt = 1.77 * 72; // 127.44 pt + const double SigHeightPt = 1.96 * 72; // 141.12 pt + + // Display size of the overlay placeholder (pixels at zoom=1.0). + // At zoom=1.0, 1 CSS px ≈ 1 pt in the DxPdfViewer render. + const double SigDisplayW = SigWidthPt; + const double SigDisplayH = SigHeightPt; + + // ── State ── + DxPdfViewer? _pdfViewer; + byte[]? _pdfBytes; + bool _pdfLoaded = false; + string _fileName = string.Empty; + string? _errorMessage; + bool _placementMode = false; + List _signatureFields = []; + + // ── PDF upload ── + async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e) + { + _errorMessage = null; + var file = e.File; + if (file is null) return; + + if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + { + _errorMessage = "Bitte wählen Sie eine PDF-Datei aus."; + return; + } + + try + { + // Max 50 MB + const long maxBytes = 50 * 1024 * 1024; + using var ms = new System.IO.MemoryStream(); + await file.OpenReadStream(maxBytes).CopyToAsync(ms); + _pdfBytes = ms.ToArray(); + _fileName = file.Name; + _pdfLoaded = true; + _signatureFields.Clear(); + _placementMode = false; + + Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _pdfBytes.Length); + } + catch (Exception ex) + { + _errorMessage = $"Fehler beim Laden der Datei: {ex.Message}"; + Logger.LogError(ex, "Failed to load PDF file"); + } + } + + // ── Placement mode ── + void TogglePlacementMode() => _placementMode = !_placementMode; + + void ClearAllFields() + { + _signatureFields.Clear(); + _placementMode = false; + } + + void RemoveField(SignatureFieldDraft field) => _signatureFields.Remove(field); + + // ── Overlay click → add signature field ── + async Task OnOverlayClickAsync(MouseEventArgs e) + { + if (!_placementMode) return; + + // Get overlay container bounds via JS + var coords = await JSRuntime.InvokeAsync( + "envelopeEditor.getClickCoords", "pdf-editor-overlay", + e.ClientX, e.ClientY); + + if (coords is null) return; + + // At zoom=1.0: container pixels ≈ PDF display pixels. + // DxPdfViewer renders at 96 dpi by default; PDF points = 72 dpi. + // Scale factor: 96/72 = 1.333 → px / 1.333 = pt + const double pxToPt = 72.0 / 96.0; + + double xPt = coords.RelX * pxToPt; + double yPt = coords.RelY * pxToPt; + + // Active page: DxPdfViewer.ActivePageIndex is 0-based + int page = (_pdfViewer?.ActivePageIndex ?? 0) + 1; + + // Display position (px on overlay) — keep in px for CSS + double displayX = coords.RelX; + double displayY = coords.RelY; + + // Prevent placing outside bounds + if (displayX < 0 || displayY < 0) return; + if (displayX + SigDisplayW > coords.ContainerW) displayX = coords.ContainerW - SigDisplayW; + if (displayY + SigDisplayH > coords.ContainerH) displayY = coords.ContainerH - SigDisplayH; + + var field = new SignatureFieldDraft(xPt, yPt, page, displayX, displayY); + _signatureFields.Add(field); + + Logger.LogInformation( + "Signature field added: Page={Page} X={X:F1}pt Y={Y:F1}pt", + page, xPt, yPt); + + // Exit placement mode after one click (user can re-click button for next) + _placementMode = false; + } + + // ── Save ── + async Task SaveAsync() + { + if (_signatureFields.Count == 0) + { + await JSRuntime.InvokeVoidAsync("console.log", + "[SenderEditor] No signature fields to save."); + return; + } + + foreach (var f in _signatureFields) + { + await JSRuntime.InvokeVoidAsync("console.log", + $"[SenderEditor] Field: Page={f.Page} X={f.XPt:F2}pt ({f.XPt/72:F3}in) Y={f.YPt:F2}pt ({f.YPt/72:F3}in)"); + } + + await JSRuntime.InvokeVoidAsync("console.log", + $"[SenderEditor] Total fields: {_signatureFields.Count}"); + } + + // ── Models ── + record SignatureFieldDraft(double XPt, double YPt, int Page, double DisplayX, double DisplayY); + + record OverlayCoords(double RelX, double RelY, double ContainerW, double ContainerH); +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js new file mode 100644 index 00000000..d8c05c61 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js @@ -0,0 +1,21 @@ +window.envelopeEditor = { + /** + * Returns click coordinates relative to the overlay element. + * @param {string} overlayId - The id of the overlay div + * @param {number} clientX - MouseEventArgs.ClientX from Blazor + * @param {number} clientY - MouseEventArgs.ClientY from Blazor + * @returns {{ relX, relY, containerW, containerH }} + */ + getClickCoords: function (overlayId, clientX, clientY) { + const el = document.getElementById(overlayId); + if (!el) return null; + + const rect = el.getBoundingClientRect(); + return { + relX: clientX - rect.left, + relY: clientY - rect.top, + containerW: rect.width, + containerH: rect.height + }; + } +}; From 762a9e8bcac169c316e3fae0c7df36676f4c105c Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 1 Jul 2026 14:26:11 +0200 Subject: [PATCH 2/2] Improve PDF viewer overlay synchronization Refactor `EnvelopeSenderEditorPage.razor` to enhance the structure and behavior of the PDF editor wrapper: - Add `class="pdf-editor-wrapper"` and update `overflow` to `auto`. - Update `DxPdfViewer`'s `CssClass` to `sender-editor-pdf-viewer`. - Introduce `OnAfterRenderAsync` to synchronize the overlay with the viewer. Add new styles in `envelope-viewer.css` for better layout: - Ensure `.pdf-editor-wrapper` and `.sender-editor-pdf-viewer` occupy full dimensions. - Center and align content within the PDF viewer. Enhance `envelope-editor.js` with `syncOverlayToPage`: - Dynamically adjust overlay position and size relative to the viewer. - Use `MutationObserver` and event listeners for real-time synchronization. - Handle delayed rendering with scheduled sync attempts. These changes improve overlay alignment, user experience, and code maintainability. --- .../Pages/EnvelopeSenderEditorPage.razor | 17 ++++- .../wwwroot/css/envelope-viewer.css | 27 +++++++ .../wwwroot/js/envelope-editor.js | 71 +++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor index fbc04868..c1cd9fca 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor @@ -147,15 +147,15 @@ else { @* PDF viewer + overlay wrapper *@ -
+
@* DxPdfViewer — zoom fixed to 1.0 for reliable coordinate mapping *@ + CssClass="sender-editor-pdf-viewer" /> @* Transparent overlay for click capture (active only in placement mode) *@
{ + const page = currentWrapper.querySelector(".dxbrv-report-preview-content"); + if (page) { + return page; + } + + return currentWrapper.querySelector(".dxbrv-report-preview-content-img") || + currentWrapper.querySelector("img.dxbrv-report-preview-content-img") || + currentWrapper.querySelector(".dxbrv-document-surface img"); + }; + + const sync = () => { + const currentWrapper = document.getElementById(wrapperId); + const currentOverlay = document.getElementById(overlayId); + + if (!currentWrapper || !currentOverlay) { + return; + } + + const target = findTarget(currentWrapper); + if (!target) { + currentOverlay.style.left = "0px"; + currentOverlay.style.top = "0px"; + currentOverlay.style.width = "0px"; + currentOverlay.style.height = "0px"; + return; + } + + const wrapperRect = currentWrapper.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + + currentOverlay.style.left = `${targetRect.left - wrapperRect.left + currentWrapper.scrollLeft}px`; + currentOverlay.style.top = `${targetRect.top - wrapperRect.top + currentWrapper.scrollTop}px`; + currentOverlay.style.width = `${targetRect.width}px`; + currentOverlay.style.height = `${targetRect.height}px`; + }; + + const scheduleSync = () => requestAnimationFrame(sync); + + const observer = new MutationObserver(scheduleSync); + observer.observe(wrapper, { childList: true, subtree: true, attributes: true }); + + wrapper.addEventListener("scroll", scheduleSync, { passive: true }); + window.addEventListener("resize", scheduleSync); + + window.envelopeEditor._overlaySyncState[overlayId] = { + sync, + observer + }; + + sync(); + setTimeout(sync, 50); + setTimeout(sync, 150); + setTimeout(sync, 400); } };