Implement drag-and-drop PDF loading via JS interop and DotNetObjectReference. Refactor file loading logic and UI structure for clarity. Add IAsyncDisposable for resource cleanup. Update pdfInterop.js to handle drop events and send PDF data to Blazor.
429 lines
13 KiB
Plaintext
429 lines
13 KiB
Plaintext
@page "/"
|
|
@using Microsoft.JSInterop
|
|
@inject IJSRuntime JS
|
|
@implements IAsyncDisposable
|
|
|
|
<div class="page-shell">
|
|
<h1>Sign PDF (Blazor)</h1>
|
|
|
|
<div class="controls">
|
|
<InputFile OnChange="HandleFileSelected" accept="application/pdf" />
|
|
<button class="btn" @onclick="ShowSignaturePad" disabled="@(!HasPdf)">Add signature</button>
|
|
<button class="btn" @onclick="() => ShowTextOverlay(false)" disabled="@(!HasPdf)">Add text</button>
|
|
<button class="btn" @onclick="() => ShowTextOverlay(true)" disabled="@(!HasPdf)">Add date</button>
|
|
<button class="btn" @onclick="Reset" disabled="@(!HasPdf)">Reset</button>
|
|
<button class="btn secondary" @onclick="Download" disabled="@(!HasPdf)">Download</button>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
|
{
|
|
<div class="error-banner">@ErrorMessage</div>
|
|
}
|
|
|
|
@if (!HasPdf)
|
|
{
|
|
<div class="drop-hint">Drop or select a PDF to start.</div>
|
|
}
|
|
|
|
@if (HasPdf)
|
|
{
|
|
<div class="document-shell" @ref="PdfHostRef" style="@($"width:{ViewportWidthPx}px")">
|
|
<canvas id="pdf-canvas" @ref="PdfCanvasRef"></canvas>
|
|
|
|
@if (ShowSignature)
|
|
{
|
|
<div class="overlay signature" @ref="OverlayRef"
|
|
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px; width:{OverlayWidthPx}px; height:{OverlayHeightPx}px;")"
|
|
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
|
|
<div class="overlay-controls">
|
|
<button class="overlay-btn" @onclick="ApplySignature" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
|
|
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
|
|
</div>
|
|
<img src="@SignatureDataUrl" draggable="false" />
|
|
</div>
|
|
}
|
|
|
|
@if (ShowText)
|
|
{
|
|
<div class="overlay text" @ref="OverlayRef"
|
|
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px;")"
|
|
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
|
|
<div class="overlay-controls">
|
|
<button class="overlay-btn" @onclick="ApplyText" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
|
|
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
|
|
</div>
|
|
<input class="overlay-input" @bind="TextValue" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation />
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="paging">
|
|
<button class="btn" @onclick="PrevPage" disabled="@(!CanPrev)"><</button>
|
|
<span>Page @DisplayPage / @PageCount</span>
|
|
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">></button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (ShowSignaturePadModal)
|
|
{
|
|
<div class="modal-backdrop">
|
|
<div class="modal">
|
|
<h3>Add signature</h3>
|
|
<canvas id="@SignatureCanvasId" width="700" height="220"></canvas>
|
|
<div class="modal-row">
|
|
<label><input type="checkbox" @bind="AutoDate" /> Auto date/time</label>
|
|
<button class="btn" @onclick="ClearSignature">Clear</button>
|
|
<span class="spacer"></span>
|
|
<button class="btn" @onclick="ConfirmSignature">Use signature</button>
|
|
<button class="btn secondary" @onclick="CloseSignaturePad">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private ElementReference PdfCanvasRef;
|
|
private ElementReference PdfHostRef;
|
|
private ElementReference OverlayRef;
|
|
|
|
private string? PdfBase64;
|
|
private string? OriginalPdfBase64;
|
|
private DotNetObjectReference<Index>? _dotNetRef;
|
|
private int PageIndex;
|
|
private int PageCount;
|
|
private double ViewportWidthPx = 800;
|
|
private double ViewportHeightPx;
|
|
|
|
private bool ShowSignaturePadModal;
|
|
private bool ShowSignature;
|
|
private bool ShowText;
|
|
private string SignatureCanvasId { get; } = $"sig-{Guid.NewGuid():N}";
|
|
private string? SignatureDataUrl;
|
|
private bool AutoDate = true;
|
|
|
|
private double OverlayXpx = 20;
|
|
private double OverlayYpx = 20;
|
|
private double OverlayWidthPx = 200;
|
|
private double OverlayHeightPx = 80;
|
|
private bool IsDragging;
|
|
private double DragStartX;
|
|
private double DragStartY;
|
|
private double StartLeft;
|
|
private double StartTop;
|
|
private string TextValue = "Text";
|
|
private string? ErrorMessage;
|
|
private DateTimeOffset _lastDragRender = DateTimeOffset.MinValue;
|
|
|
|
private bool HasPdf => !string.IsNullOrWhiteSpace(PdfBase64);
|
|
private int DisplayPage => PageIndex + 1;
|
|
private bool CanPrev => PageIndex > 0;
|
|
private bool CanNext => PageIndex + 1 < PageCount;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
|
|
_dotNetRef ??= DotNetObjectReference.Create(this);
|
|
await JS.InvokeVoidAsync("pdfInterop.registerDropHandler", _dotNetRef);
|
|
}
|
|
|
|
if (ShowSignaturePadModal)
|
|
{
|
|
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
|
|
}
|
|
}
|
|
|
|
private async Task HandleFileSelected(InputFileChangeEventArgs e)
|
|
{
|
|
if (e.FileCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await LoadPdfFromBrowserFile(e.File);
|
|
}
|
|
|
|
private async Task LoadPdfFromBrowserFile(IBrowserFile file)
|
|
{
|
|
ErrorMessage = null;
|
|
|
|
try
|
|
{
|
|
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
|
|
using var ms = new MemoryStream();
|
|
await stream.CopyToAsync(ms);
|
|
await LoadPdfFromBase64Internal(Convert.ToBase64String(ms.ToArray()));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
|
|
PdfBase64 = null;
|
|
PageCount = 0;
|
|
PageIndex = 0;
|
|
}
|
|
}
|
|
|
|
[JSInvokable]
|
|
public Task LoadPdfFromBase64(string base64)
|
|
{
|
|
return LoadPdfFromBase64Internal(base64);
|
|
}
|
|
|
|
private async Task LoadPdfFromBase64Internal(string base64)
|
|
{
|
|
ErrorMessage = null;
|
|
PdfBase64 = base64;
|
|
OriginalPdfBase64 = PdfBase64;
|
|
|
|
// Show the canvas before we start rendering
|
|
await InvokeAsync(StateHasChanged);
|
|
await Task.Yield();
|
|
|
|
// Make sure pdf.js is ready
|
|
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
|
|
|
|
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
|
|
PageCount = result.Pages;
|
|
PageIndex = 0;
|
|
|
|
await RenderPage();
|
|
}
|
|
|
|
private async Task RenderPage()
|
|
{
|
|
if (!HasPdf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var viewport = await JS.InvokeAsync<ViewportInfo>("pdfInterop.renderPage", PageIndex, "pdf-canvas", ViewportWidthPx);
|
|
ViewportWidthPx = viewport.Width;
|
|
ViewportHeightPx = viewport.Height;
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ErrorMessage = $"Fehler beim Rendern: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private async Task Reset()
|
|
{
|
|
ErrorMessage = null;
|
|
CloseOverlays();
|
|
ShowSignaturePadModal = false;
|
|
OverlayXpx = 20;
|
|
OverlayYpx = 20;
|
|
OverlayWidthPx = 200;
|
|
OverlayHeightPx = 80;
|
|
TextValue = "Text";
|
|
|
|
if (string.IsNullOrWhiteSpace(OriginalPdfBase64))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PdfBase64 = OriginalPdfBase64;
|
|
PageIndex = 0;
|
|
|
|
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
|
|
PageCount = result.Pages;
|
|
await RenderPage();
|
|
}
|
|
|
|
private void CloseOverlays()
|
|
{
|
|
ShowSignature = false;
|
|
ShowText = false;
|
|
SignatureDataUrl = null;
|
|
}
|
|
|
|
private void ShowSignaturePad()
|
|
{
|
|
ShowSignaturePadModal = true;
|
|
}
|
|
|
|
private async Task ConfirmSignature()
|
|
{
|
|
SignatureDataUrl = await JS.InvokeAsync<string>("pdfInterop.getSignatureDataUrl", SignatureCanvasId);
|
|
if (string.IsNullOrWhiteSpace(SignatureDataUrl))
|
|
{
|
|
return;
|
|
}
|
|
|
|
OverlayWidthPx = 200;
|
|
OverlayHeightPx = 80;
|
|
OverlayXpx = 20;
|
|
OverlayYpx = 20;
|
|
ShowSignature = true;
|
|
ShowText = false;
|
|
ShowSignaturePadModal = false;
|
|
}
|
|
|
|
private void CloseSignaturePad()
|
|
{
|
|
ShowSignaturePadModal = false;
|
|
}
|
|
|
|
private void ClearSignature()
|
|
{
|
|
JS.InvokeVoidAsync("pdfInterop.clearSignaturePad", SignatureCanvasId);
|
|
}
|
|
|
|
private void ShowTextOverlay(bool autoDate)
|
|
{
|
|
TextValue = autoDate ? DateTimeOffset.Now.ToString("M/d/yyyy HH:mm:ss zzz") : "Text";
|
|
OverlayWidthPx = 240;
|
|
OverlayHeightPx = 40;
|
|
OverlayXpx = 20;
|
|
OverlayYpx = 20;
|
|
ShowText = true;
|
|
ShowSignature = false;
|
|
}
|
|
|
|
private void StartDrag(PointerEventArgs args)
|
|
{
|
|
IsDragging = true;
|
|
DragStartX = args.ClientX;
|
|
DragStartY = args.ClientY;
|
|
StartLeft = OverlayXpx;
|
|
StartTop = OverlayYpx;
|
|
|
|
if (OverlayRef.Context != null)
|
|
{
|
|
JS.InvokeVoidAsync("pdfInterop.capturePointer", OverlayRef, args.PointerId);
|
|
}
|
|
}
|
|
|
|
private void OnDrag(PointerEventArgs args)
|
|
{
|
|
if (!IsDragging)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dx = args.ClientX - DragStartX;
|
|
var dy = args.ClientY - DragStartY;
|
|
OverlayXpx = StartLeft + dx;
|
|
OverlayYpx = StartTop + dy;
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
if (now - _lastDragRender > TimeSpan.FromMilliseconds(16))
|
|
{
|
|
_lastDragRender = now;
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
private void EndDrag(PointerEventArgs args)
|
|
{
|
|
IsDragging = false;
|
|
|
|
if (OverlayRef.Context != null)
|
|
{
|
|
JS.InvokeVoidAsync("pdfInterop.releasePointer", OverlayRef, args.PointerId);
|
|
}
|
|
}
|
|
|
|
private async Task ApplySignature()
|
|
{
|
|
if (SignatureDataUrl is null || !HasPdf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applySignature", new
|
|
{
|
|
base64 = PdfBase64,
|
|
pageIndex = PageIndex,
|
|
left = OverlayXpx,
|
|
top = OverlayYpx,
|
|
width = OverlayWidthPx,
|
|
height = OverlayHeightPx,
|
|
renderWidth = ViewportWidthPx,
|
|
renderHeight = ViewportHeightPx,
|
|
dataUrl = SignatureDataUrl,
|
|
autoDate = AutoDate,
|
|
});
|
|
|
|
CloseOverlays();
|
|
await RenderPage();
|
|
}
|
|
|
|
private async Task ApplyText()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(TextValue) || !HasPdf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applyText", new
|
|
{
|
|
base64 = PdfBase64,
|
|
pageIndex = PageIndex,
|
|
left = OverlayXpx,
|
|
top = OverlayYpx,
|
|
width = OverlayWidthPx,
|
|
height = OverlayHeightPx,
|
|
renderWidth = ViewportWidthPx,
|
|
renderHeight = ViewportHeightPx,
|
|
text = TextValue,
|
|
fontSize = 20,
|
|
});
|
|
|
|
CloseOverlays();
|
|
await RenderPage();
|
|
}
|
|
|
|
private void CancelOverlay()
|
|
{
|
|
CloseOverlays();
|
|
}
|
|
|
|
private async Task PrevPage()
|
|
{
|
|
if (!CanPrev)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PageIndex--;
|
|
await RenderPage();
|
|
}
|
|
|
|
private async Task NextPage()
|
|
{
|
|
if (!CanNext)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PageIndex++;
|
|
await RenderPage();
|
|
}
|
|
|
|
private async Task Download()
|
|
{
|
|
if (!HasPdf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await JS.InvokeVoidAsync("pdfInterop.downloadPdf", PdfBase64, "document-signed.pdf");
|
|
}
|
|
|
|
private record RenderResult(int Pages);
|
|
|
|
private record ViewportInfo(double Width, double Height, double PageWidth, double PageHeight);
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
_dotNetRef?.Dispose();
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|