Refactor PDF editor with custom rendering

Replaced `DxPdfViewer` with a custom PDF rendering solution in `SenderPdfImageEditor` to improve flexibility and control. Added support for displaying placeholder messages and interactive signature fields. Introduced the `SenderPdfFieldOverlay` record to encapsulate field data.

Updated `SenderEditorPage` logic to manage page counts, image URLs, and field overlays. Added a method to estimate PDF page counts. Refactored field removal logic to use index-based operations.

Enhanced styling in `envelope-viewer.css` for improved layout and responsiveness. Updated localization files with new strings for page labels and placeholders. Adjusted JavaScript selectors in `envelope-editor.js` to support the new rendering approach.
This commit is contained in:
2026-09-29 16:01:42 +02:00
parent 4bd0cc2e94
commit ed44d841e7
8 changed files with 262 additions and 28 deletions

View File

@@ -276,10 +276,13 @@
}
else
{
<SenderPdfImageEditor PdfBytes="@_pdfBytes"
IsPlacementMode="@(_pendingReceiverForPlacement is not null)"
ViewerCssClass="@ViewerCssClass"
EditorClick="OnPdfAreaClickAsync" />
<SenderPdfImageEditor IsPlacementMode="@(_pendingReceiverForPlacement is not null)"
PageCount="@_pageCount"
PageImageDataUrls="@_pageImageDataUrls"
PdfDataUrl="@PdfDataUrl"
Fields="@FieldOverlays"
EditorClick="OnPdfAreaClickAsync"
RemoveField="OnRemoveFieldByIndexAsync" />
}
</div>
</div>
@@ -459,15 +462,16 @@
const double SigWidthPt = 1.77 * 72; // 127.44 pt
const double SigHeightPt = 1.96 * 72; // 141.12 pt
// CssClass for DxPdfViewer — used by JS to locate page elements
// CssClass for JS click coordinate helper
const string ViewerCssClass = "sender-editor-pdf-viewer";
// ── State ──
bool _pdfLoaded = false;
string _fileName = string.Empty;
string? _errorMessage;
byte[]? _pdfBytes; // Current rendered PDF (original + placeholders burned in)
byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw
int _pageCount;
Dictionary<int, string> _pageImageDataUrls = [];
int? _loadedEnvelopeId;
int? _editingEnvelopeId;
bool _isLoadingEnvelopeForEdit;
@@ -475,6 +479,14 @@
List<SignatureFieldDraft> _signatureFields = [];
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
IReadOnlyList<SenderPdfFieldOverlay> FieldOverlays => _signatureFields
.Select((f, i) => new SenderPdfFieldOverlay(i, f.Page, f.XPt, f.YPt, f.ReceiverName, f.Color))
.ToList();
string? PdfDataUrl => _originalPdfBytes is { Length: > 0 }
? $"data:application/pdf;base64,{Convert.ToBase64String(_originalPdfBytes)}"
: null;
// ── Save state ──
bool _isSaving = false;
bool _savePopupVisible = false;
@@ -582,7 +594,9 @@
_originalPdfBytes = document?.ByteData;
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
_pdfBytes = _pdfLoaded ? _originalPdfBytes : null;
_pageCount = EstimatePdfPageCount(_originalPdfBytes) ??
Math.Max(1, _signatureFields.Select(f => f.Page).DefaultIfEmpty(1).Max());
_pageImageDataUrls.Clear();
_fileName = $"Envelope-{envelopeId}.pdf";
_pendingReceiverForPlacement = null;
_loadedEnvelopeId = envelopeId;
@@ -624,12 +638,11 @@
_originalPdfBytes = ms.ToArray();
_fileName = file.Name;
_pdfLoaded = true;
_pageCount = EstimatePdfPageCount(_originalPdfBytes) ?? 1;
_pageImageDataUrls.Clear();
_signatureFields.Clear();
_pendingReceiverForPlacement = null;
// Rendered PDF starts as the clean original (no placeholders yet)
_pdfBytes = _originalPdfBytes;
Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _originalPdfBytes.Length);
}
catch (Exception ex)
@@ -693,19 +706,18 @@
_signatureFields.Add(field);
_pendingReceiverForPlacement = null;
// Keep original PDF bytes unchanged. Signature boxes are tracked in-memory.
_pdfBytes = _originalPdfBytes;
Logger.LogInformation(
"[SenderEditor] Field added: Page={Page} X={X:F1}pt Y={Y:F1}pt Receiver={Receiver}",
page1Based, xPt, yPt, field.ReceiverName);
}
// ── Remove a single field ──
async Task RemoveFieldAsync(SignatureFieldDraft field)
async Task OnRemoveFieldByIndexAsync(int index)
{
_signatureFields.Remove(field);
_pdfBytes = _originalPdfBytes;
if (index < 0 || index >= _signatureFields.Count)
return;
_signatureFields.RemoveAt(index);
await Task.CompletedTask;
}
@@ -714,10 +726,39 @@
{
_signatureFields.Clear();
_pendingReceiverForPlacement = null;
_pdfBytes = _originalPdfBytes;
await Task.CompletedTask;
}
static int? EstimatePdfPageCount(byte[]? documentBytes)
{
if (documentBytes is null || documentBytes.Length == 0)
return null;
try
{
var text = System.Text.Encoding.ASCII.GetString(documentBytes);
const string marker = "/Type /Page";
var count = 0;
var index = 0;
while (true)
{
index = text.IndexOf(marker, index, StringComparison.Ordinal);
if (index < 0)
break;
count++;
index += marker.Length;
}
return count > 0 ? count : null;
}
catch
{
return null;
}
}
void Cancel() => NavigationManager.NavigateTo("/sender");
void GoToDashboard()