Compare commits

12 Commits

Author SHA1 Message Date
OlgunR
e6b41f10c8 Merge branch 'feat/receiver-ui' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/receiver-ui 2025-12-10 11:16:27 +01:00
OlgunR
dc78ad4a24 Improve canvas mouse position accuracy; add settings.json
Refactored mouse event handling in pdfInterop.js to use a new getPos helper, ensuring accurate coordinate mapping on scaled or resized canvases. Updated start and move functions to use this helper. Added an empty settings.json file.
2025-12-10 11:16:13 +01:00
b282318298 Add launchSettings.json for ReceiverUIBlazor project
Configured development launch profile with browser launch, environment variable, and HTTP/HTTPS URLs for EnvelopeGenerator.ReceiverUIBlazor.
2025-12-10 09:55:32 +01:00
8d68ea8c57 Add IIS publish profile for EnvelopeGenerator.ReceiverUIBlazor
Introduced IISProfile.pubxml to enable packaging and deployment
of the EnvelopeGenerator.ReceiverUIBlazor project to IIS. The
profile configures publish method, build settings, output
package location, and IIS app path.
2025-12-10 09:55:24 +01:00
OlgunR
7cb8b02b1d Revamp UI colors and fonts for warmer, modern look
Switched to "Red Hat Text" and "Teko" fonts via Google Fonts. Updated color palette from cool blues to warm reds and yellows, including backgrounds, accents, and error states. Refreshed button, top bar, and card styles for improved visual hierarchy and softer appearance.
2025-12-09 10:58:53 +01:00
OlgunR
cc1d33462c Add drag-and-drop PDF support to Blazor app
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.
2025-12-09 10:42:11 +01:00
OlgunR
cd85b4fffc Improve PDF reset logic and modernize UI styling
Refactor PDF reset to restore original document state using a new OriginalPdfBase64 variable and async logic. Redesign app.css with a lighter color palette, CSS variables, and updated styles for buttons, overlays, modals, and inputs for a cleaner, more accessible UI. Adjust signature and text overlay colors in pdfInterop.js for better contrast and consistency.
2025-12-09 10:26:01 +01:00
OlgunR
ab3e7fb4e9 Error while dragging fields fixed 2025-12-09 09:50:10 +01:00
OlgunR
4a7676765c Fixed error while loading file 2025-12-09 08:57:39 +01:00
OlgunR
562ceb9c3f First results converting receiver-ui-react into a Blazor Web App 2025-12-08 16:21:10 +01:00
OlgunR
751ea706df Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:06:26 +01:00
OlgunR
490ad9f7cf Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:04:51 +01:00
13 changed files with 1119 additions and 0 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>EnvelopeGenerator.ReceiverUIBlazor</AssemblyName>
<RootNamespace>EnvelopeGenerator.ReceiverUIBlazor</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,428 @@
@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)">&lt;</button>
<span>Page @DisplayPage / @PageCount</span>
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">&gt;</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;
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.ReceiverUIBlazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>7f262ad4-53b1-42d3-9a5f-132cf50f150c</ProjectGuid>
<DesktopBuildPackageLocation>E:\TekH\Visual Studio\src\EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>ReceiverUIBlazor</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"EnvelopeGenerator.ReceiverUIBlazor": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:49582;http://localhost:49583"
}
}
}

View File

@@ -0,0 +1,10 @@
@inherits LayoutComponentBase
<div class="main-layout">
<header class="top-bar">
<div class="brand">Receiver UI (Blazor)</div>
</header>
<main class="content">
@Body
</main>
</div>

View File

@@ -0,0 +1,8 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUIBlazor
@using EnvelopeGenerator.ReceiverUIBlazor.Shared

View File

@@ -0,0 +1,241 @@
@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;500;600;700&family=Teko:wght@500;600&display=swap');
:root {
--bg: #f7f7f8;
--bg-strong: #fff6f6;
--text: #474747;
--muted: #777777;
--border: #e7e7e7;
--shadow: 0 18px 55px rgba(20, 20, 20, 0.08);
--card: #ffffff;
--accent: #a52431;
--accent-strong: #8d1e2a;
--accent-soft: #f8e5e8;
--highlight: #ffd62f;
--danger: #a52431;
}
body {
margin: 0;
font-family: "Red Hat Text", "Segoe UI", system-ui, -apple-system, sans-serif;
background: radial-gradient(120% 120% at 6% 12%, var(--bg-strong) 0%, #fffdf7 45%, var(--bg) 85%);
color: var(--text);
line-height: 1.5;
}
.main-layout {
min-height: 100vh;
}
.top-bar {
display: flex;
align-items: center;
padding: 14px 24px;
background: var(--accent);
border-bottom: 1px solid var(--accent-strong);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
color: #ffd62f;
}
.top-bar .brand {
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
letter-spacing: 0.6px;
color: #ffd62f;
}
.content {
padding: 28px 32px 40px;
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
h1 {
margin: 0 0 10px;
letter-spacing: 0.2px;
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
justify-content: center;
}
.btn {
background: #4a4a4a;
color: #ffffff;
border: 1px solid #404040;
padding: 10px 15px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
background: #3f3f3f;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.btn.secondary {
background: #f4f4f4;
color: #474747;
border: 1px solid #d3d3d3;
box-shadow: none;
}
.btn.secondary:hover:not(:disabled) {
background: #e9e9e9;
transform: translateY(-1px);
}
.drop-hint {
padding: 26px;
border: 1px dashed var(--border);
border-radius: 14px;
text-align: center;
color: var(--muted);
background: #ffffff;
width: min(1100px, 100%);
margin: 0 auto;
box-shadow: 0 8px 18px rgba(165, 36, 49, 0.06);
}
.error-banner {
margin-top: 10px;
padding: 12px 14px;
border-radius: 10px;
background: #fcebec;
border: 1px solid #f3c6cd;
color: var(--accent-strong);
}
.document-shell {
position: relative;
margin-top: 14px;
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--card);
border: 1px solid var(--border);
margin-left: auto;
margin-right: auto;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #ffffff;
}
.overlay {
position: absolute;
border: 1px dashed var(--accent);
border-radius: 10px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 12px 30px rgba(165, 36, 49, 0.15);
user-select: none;
touch-action: none;
}
.overlay.signature img {
display: block;
max-width: 100%;
max-height: 100%;
}
.overlay-controls {
position: absolute;
top: -44px;
right: 0;
display: flex;
gap: 6px;
}
.overlay-btn {
background: #ffffff;
color: var(--accent-strong);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
box-shadow: 0 6px 16px rgba(165, 36, 49, 0.16);
}
.overlay-input {
border: 1px solid var(--border);
background: #ffffff;
color: var(--text);
font-size: 18px;
padding: 6px 8px;
min-width: 180px;
border-radius: 8px;
outline: none;
}
.paging {
margin-top: 14px;
display: flex;
gap: 10px;
align-items: center;
color: var(--muted);
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 18px;
min-width: 760px;
box-shadow: var(--shadow);
}
.modal-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
color: var(--muted);
}
.modal canvas {
background: #ffffff;
border: 1px solid var(--border);
border-radius: 10px;
}
.spacer {
flex: 1;
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Receiver UI (Blazor)</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<!-- pdf.js 3.11 UMD + classic worker for compatibility; SRI removed to avoid digest mismatches -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<script src="js/pdfInterop.js"></script>
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -0,0 +1,339 @@
(function () {
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
const state = {
pdfDoc: null,
pdfBytes: null,
lastViewport: null,
pdfJsReady: null,
};
function base64ToUint8(base64) {
const binStr = atob(base64);
const len = binStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binStr.charCodeAt(i);
}
return bytes;
}
async function reloadFromBase64(base64) {
state.pdfBytes = base64ToUint8(base64);
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
return { pages: state.pdfDoc.numPages };
}
function dataUrlDownload(dataUrl, filename) {
const a = document.createElement('a');
a.href = dataUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const pointerPads = new Map();
function loadScriptOnce(url) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
if (existing && existing.dataset.loaded === "true") {
resolve();
return;
}
const script = existing || document.createElement('script');
script.src = url;
script.defer = true;
script.onload = () => {
script.dataset.loaded = "true";
resolve();
};
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
if (!existing) {
document.head.appendChild(script);
}
});
}
async function ensurePdfJsLoaded() {
if (typeof pdfjsLib !== "undefined") {
return;
}
if (!state.pdfJsReady) {
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
}
await state.pdfJsReady;
if (typeof pdfjsLib === "undefined") {
throw new Error("pdfjsLib could not be loaded");
}
}
window.pdfInterop = {
ensureReady: async () => {
// Ensure pdf.js is present and the worker path is set explicitly.
await ensurePdfJsLoaded();
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
}
} else {
throw new Error("pdf.js not available after load");
}
},
loadPdf: async (base64) => {
await ensurePdfJsLoaded();
try {
const result = await reloadFromBase64(base64);
if (!result || !result.pages) {
throw new Error("PDF has keine Seiten erkannt");
}
return result;
} catch (err) {
console.error("pdfInterop.loadPdf failed", err);
throw err;
}
},
renderPage: async (pageIndex, canvasId, targetWidth) => {
await ensurePdfJsLoaded();
if (!state.pdfDoc) {
throw new Error('PDF not loaded');
}
const page = await state.pdfDoc.getPage(pageIndex + 1);
const rawViewport = page.getViewport({ scale: 1 });
const scale = targetWidth / rawViewport.width;
const viewport = page.getViewport({ scale });
let canvas = document.getElementById(canvasId);
if (!canvas) {
// give the UI a tiny delay to render the canvas into the DOM
await new Promise(r => setTimeout(r, 40));
canvas = document.getElementById(canvasId);
}
if (!canvas) {
console.error("renderPage: canvas not found", canvasId);
throw new Error('Canvas not found');
}
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
state.lastViewport = {
width: viewport.width,
height: viewport.height,
pageWidth: rawViewport.width,
pageHeight: rawViewport.height,
};
return state.lastViewport;
},
applySignature: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
dataUrl,
autoDate,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const pngImage = await pdfDoc.embedPng(dataUrl);
const drawWidth = width * scaleX;
const drawHeight = height * scaleY;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawImage(pngImage, {
x,
y,
width: drawWidth,
height: drawHeight,
});
if (autoDate) {
const text = `Signed ${new Date().toLocaleString()}`;
page.drawText(text, {
x,
y: y - 14 * scaleY,
size: 14 * scaleX,
color: PDFLib.rgb(0.11, 0.25, 0.56),
});
}
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
applyText: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
text,
fontSize,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawText(text, {
x,
y,
size: fontSize * scaleX,
color: PDFLib.rgb(0.2, 0.23, 0.28),
});
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
downloadPdf: (base64, filename) => {
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
},
initSignaturePad: (canvasId) => {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#1c3d8f';
const padState = {
drawing: false,
lastX: 0,
lastY: 0,
};
function getPos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width ? canvas.width / rect.width : 1;
const scaleY = rect.height ? canvas.height / rect.height : 1;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY,
};
}
function start(e) {
padState.drawing = true;
const pos = getPos(e);
padState.lastX = pos.x;
padState.lastY = pos.y;
}
function move(e) {
if (!padState.drawing) return;
const pos = getPos(e);
const x = pos.x;
const y = pos.y;
ctx.beginPath();
ctx.moveTo(padState.lastX, padState.lastY);
ctx.lineTo(x, y);
ctx.stroke();
padState.lastX = x;
padState.lastY = y;
}
function end() {
padState.drawing = false;
}
canvas.onpointerdown = start;
canvas.onpointermove = move;
canvas.onpointerup = end;
canvas.onpointerleave = end;
pointerPads.set(canvasId, { ctx, canvas });
},
registerDropHandler: (dotNetRef) => {
if (window.__pdfDropRegistered) return;
window.__pdfDropRegistered = true;
const prevent = (e) => {
e.preventDefault();
e.stopPropagation();
};
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
document.addEventListener(evt, prevent, false);
});
document.addEventListener('drop', (e) => {
prevent(e);
const files = e.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result === 'string') {
const base64 = result.split(',')[1] || result;
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
}
};
reader.readAsDataURL(file);
}, false);
},
clearSignaturePad: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return;
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
},
getSignatureDataUrl: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return null;
return pad.canvas.toDataURL('image/png');
},
capturePointer: (element, pointerId) => {
if (element && element.setPointerCapture) {
try {
element.setPointerCapture(pointerId);
} catch (err) {
console.warn('capturePointer failed', err);
}
}
},
releasePointer: (element, pointerId) => {
if (element && element.releasePointerCapture) {
try {
element.releasePointerCapture(pointerId);
} catch (err) {
console.warn('releasePointer failed', err);
}
}
}
};
})();

View File

@@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{34AF8679-E8AF-4AB6-B861-53ED241B1228}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUIBlazor", "EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.csproj", "{7F262AD4-53B1-42D3-9A5F-132CF50F150C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -97,6 +99,10 @@ Global
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Release|Any CPU.Build.0 = Release|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -118,6 +124,7 @@ Global
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{34AF8679-E8AF-4AB6-B861-53ED241B1228} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{7F262AD4-53B1-42D3-9A5F-132CF50F150C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}