361 lines
10 KiB
Plaintext
361 lines
10 KiB
Plaintext
@page "/"
|
|
@inject IJSRuntime JS
|
|
|
|
<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" 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">✔</button>
|
|
<button class="overlay-btn" @onclick="CancelOverlay">✖</button>
|
|
</div>
|
|
<img src="@SignatureDataUrl" draggable="false" />
|
|
</div>
|
|
}
|
|
@if (ShowText)
|
|
{
|
|
<div class="overlay text" 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">✔</button>
|
|
<button class="overlay-btn" @onclick="CancelOverlay">✖</button>
|
|
</div>
|
|
<input class="overlay-input" @bind="TextValue" />
|
|
</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>
|
|
}
|
|
|
|
@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 string? PdfBase64;
|
|
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 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");
|
|
}
|
|
|
|
if (ShowSignaturePadModal)
|
|
{
|
|
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
|
|
}
|
|
}
|
|
|
|
private async Task HandleFileSelected(InputFileChangeEventArgs e)
|
|
{
|
|
ErrorMessage = null;
|
|
|
|
try
|
|
{
|
|
if (e.FileCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var file = e.File;
|
|
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
|
|
using var ms = new MemoryStream();
|
|
await stream.CopyToAsync(ms);
|
|
PdfBase64 = Convert.ToBase64String(ms.ToArray());
|
|
|
|
// 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();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
|
|
PdfBase64 = null;
|
|
PageCount = 0;
|
|
PageIndex = 0;
|
|
}
|
|
}
|
|
|
|
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 void Reset()
|
|
{
|
|
PdfBase64 = null;
|
|
PageIndex = 0;
|
|
PageCount = 0;
|
|
ViewportHeightPx = 0;
|
|
CloseOverlays();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private void OnDrag(PointerEventArgs args)
|
|
{
|
|
if (!IsDragging)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dx = args.ClientX - DragStartX;
|
|
var dy = args.ClientY - DragStartY;
|
|
OverlayXpx = StartLeft + dx;
|
|
OverlayYpx = StartTop + dy;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void EndDrag(PointerEventArgs args)
|
|
{
|
|
IsDragging = false;
|
|
}
|
|
|
|
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);
|
|
}
|