From b9cd6297669839cafa34e7050e549853f2887767 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 20 Oct 2026 17:04:15 +0200 Subject: [PATCH] Add e-signature support and improve PDF signing process Introduced `ESignParams` for configurable e-signature settings, including `ApplicationName` and `DefaultReason`. Updated `DocReceiverElementDto` and `PdfSignOptionsDto` to support new properties. Registered `ESignParams` in DI container. Refactored `SignPdfByElementCommand` to include new fields and improved error handling. Added `SignPdfByEnvelopeReceiverCommand` to enable signing documents for envelope receivers. Replaced DevExpress with PdfSharp for PDF rendering in `ReceiverSignedPage.razor`, improving layout and flexibility. Enhanced `PdfSignatureController` with a new endpoint for signing documents by envelope key. Added `DataIntegrityException` for better error handling. Updated `appsettings.json` with `ESignParams` configuration. --- .../Common/Configurations/ESignParams.cs | 17 ++ .../Common/Dto/DocReceiverElementDto.cs | 9 +- .../Common/Dto/PdfSignOptionsDto.cs | 4 +- .../DependencyInjection.cs | 1 + .../ReadEnvelopeReceiverSecretQuery.cs | 19 ++- .../PdfSigning/SignPdfByElementCommand.cs | 74 +++++++-- .../SignPdfByEnvelopeReceiverCommand.cs | 89 ++++++++++ .../Exceptions/DataIntegrityException.cs | 13 ++ .../Components/Pages/ReceiverSignedPage.razor | 154 +++++++++--------- .../Controllers/PdfSignatureController.cs | 39 ++++- .../EnvelopeGenerator.Server/appsettings.json | 4 + 11 files changed, 327 insertions(+), 96 deletions(-) create mode 100644 EnvelopeGenerator.Application/Common/Configurations/ESignParams.cs create mode 100644 EnvelopeGenerator.Application/PdfSigning/SignPdfByEnvelopeReceiverCommand.cs create mode 100644 EnvelopeGenerator.Domain/Exceptions/DataIntegrityException.cs diff --git a/EnvelopeGenerator.Application/Common/Configurations/ESignParams.cs b/EnvelopeGenerator.Application/Common/Configurations/ESignParams.cs new file mode 100644 index 00000000..3f454cdf --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Configurations/ESignParams.cs @@ -0,0 +1,17 @@ +namespace EnvelopeGenerator.Application.Common.Configurations; + +/// +/// +/// +public class ESignParams +{ + /// + /// + /// + public required string ApplicationName { get; set; } + + /// + /// + /// + public string DefaultReason { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs b/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs index be38a95c..df8ef0e7 100644 --- a/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs @@ -1,8 +1,10 @@ -using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Application.Common.Dto.Receiver; +using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Interfaces; namespace EnvelopeGenerator.Application.Common.Dto; +// TODO: Add the “Reason” property and integrate with UI and the digital signature process. /// /// Data Transfer Object representing a positioned element assigned to a document receiver. /// @@ -28,6 +30,11 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload /// public int ReceiverId { get; set; } + /// + /// + /// + public virtual ReceiverDto? Receiver { get; set; } + /// /// Gets or sets the type of the element. /// diff --git a/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs b/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs index 104e2537..8c08b171 100644 --- a/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs @@ -30,12 +30,12 @@ public sealed record PdfSignOptionsDto /// /// /// - public string Email { get; set; } = null!; + public string? Email { get; set; } /// /// /// - public string Phone { get; set; } = null!; + public string? Phone { get; set; } /// /// diff --git a/EnvelopeGenerator.Application/DependencyInjection.cs b/EnvelopeGenerator.Application/DependencyInjection.cs index 97c9757c..08188fc9 100644 --- a/EnvelopeGenerator.Application/DependencyInjection.cs +++ b/EnvelopeGenerator.Application/DependencyInjection.cs @@ -51,6 +51,7 @@ public static class DependencyInjection services.Configure(config.GetSection(nameof(MailParams))); services.Configure(config.GetSection(nameof(AuthenticatorParams))); services.Configure(config.GetSection(nameof(TotpSmsParams))); + services.Configure(config.GetSection(nameof(ESignParams))); services.AddHttpClientService(config.GetSection(nameof(GtxMessagingParams))); services.TryAddScoped(); // Changed: Singleton → Scoped diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs index e9fd1c20..b3bc2ad0 100644 --- a/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadEnvelopeReceiverSecretQuery.cs @@ -21,7 +21,24 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries; /// public record ReadEnvelopeReceiverSecretQuery : EnvelopeReceiverQueryBase, - IRequest; + IRequest +{ + /// + /// + /// + public ReadEnvelopeReceiverSecretQuery() { } + + /// + /// + /// + /// + /// + public ReadEnvelopeReceiverSecretQuery(int envelopeId, int receiverId) + { + Envelope.Id = envelopeId; + Receiver.Id = receiverId; + } +} /// /// Extension methods for dispatching via . diff --git a/EnvelopeGenerator.Application/PdfSigning/SignPdfByElementCommand.cs b/EnvelopeGenerator.Application/PdfSigning/SignPdfByElementCommand.cs index 988973a4..8b296bc2 100644 --- a/EnvelopeGenerator.Application/PdfSigning/SignPdfByElementCommand.cs +++ b/EnvelopeGenerator.Application/PdfSigning/SignPdfByElementCommand.cs @@ -1,9 +1,12 @@ using DigitalData.Core.Exceptions; +using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Enums; using EnvelopeGenerator.Application.Common.Interfaces.Doc; using EnvelopeGenerator.Application.Common.Interfaces.Services; using EnvelopeGenerator.Application.DocReceiverElements.Queries; using MediatR; +using Microsoft.Extensions.Options; namespace EnvelopeGenerator.Application.PdfSigning; @@ -21,13 +24,36 @@ public record SignPdfByElementCommand : IRequest /// /// public int? ElementId { get; init; } + + /// + /// Custom document + /// + public Stream? Document { get; set; } + + /// + /// + /// + public string? Phone { get; set; } + + /// + /// + /// + public string? Reason { get; set; } + + /// + /// + /// + public PdfSignatureCertificationLevel CertificationLevel { get; init; } = PdfSignatureCertificationLevel.NoChangesAllowed; } /// /// /// +/// /// -public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer) : IRequestHandler +/// +/// +public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer, IOptionsMonitor options) : IRequestHandler { /// /// @@ -37,49 +63,71 @@ public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService s /// public async Task Handle(SignPdfByElementCommand request, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // ensure element - if(request.Element is null) + if (request.Element is null) { var elements = await sender.Send(new ReadDocReceiverElementQuery() { Id = request.ElementId!, - IncludeDocument= true + IncludeDocument = request.Document is null }, cancellationToken); - if (!elements.Any()) - throw new NotFoundException($"Element with ID {request.ElementId} not found."); - - request.Element = elements.FirstOrDefault(); + request.Element = elements.FirstOrDefault() ?? throw new NotFoundException($"Element with ID {request.ElementId} not found."); ; } // ensure document - var doc = request.Element!.Document; + if(request.Document is null) + { + if (request.Element?.Document?.ByteData is not { Length: > 0 }) + throw new NotFoundException("Document data is missing or empty."); - if(doc is null || doc.ByteData is null || doc.ByteData.Length == 0) - throw new NotFoundException("Document data is missing or empty."); + request.Document = new MemoryStream(request.Element.Document.ByteData); + } + + ValidateAfterDataIsComplete(request); // create signature image from element var img = imgRenderer.ComposeSignaturePreviewPng(request.Element); // sign pdf - var inputPdf = new MemoryStream(doc.ByteData); var outputPdf = new MemoryStream(); await signService.SignPdfAsync( - inputPdf: inputPdf, + inputPdf: request.Document, outputPdf: outputPdf, options: new PdfSignOptionsDto { + Name = request.Element.FullName!, + Location = request.Element.Position!, + Phone = request.Phone, + Reason = request.Reason ?? options.CurrentValue.DefaultReason, + Email = request.Element.Receiver!.EmailAddress, + ApplicationName = options.CurrentValue.ApplicationName, + DocumentElementId = request.Element.Id, + + CertificationLevel = request.CertificationLevel, Appearance = new PdfSignatureAppearanceDto( PageNumber: request.Element.Page, Left: request.Element.Left, Bottom: request.Element.Y, Width: request.Element.Width, - Height: request.Element.Height) + Height: request.Element.Height, + CoordinateUnit: PdfSignatureCoordinateUnit.Inch, + CoordinateOrigin: PdfSignatureCoordinateOrigin.TopLeft) }, image: img, cancellationToken: cancellationToken); return outputPdf; } + + private static void ValidateAfterDataIsComplete(SignPdfByElementCommand request) + { + if(request.Element!.FullName is null) + throw new BadRequestException($"Document is not signed. FullName is required. Element ID: {request.Element.Id}"); + else if(request.Element.Position is null) + throw new BadRequestException($"Document is not signed. Position is required. Element ID: {request.Element.Id}"); + } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/PdfSigning/SignPdfByEnvelopeReceiverCommand.cs b/EnvelopeGenerator.Application/PdfSigning/SignPdfByEnvelopeReceiverCommand.cs new file mode 100644 index 00000000..3ccc464f --- /dev/null +++ b/EnvelopeGenerator.Application/PdfSigning/SignPdfByEnvelopeReceiverCommand.cs @@ -0,0 +1,89 @@ +using AutoMapper; +using DigitalData.Core.Abstraction.Application.Repository; +using DigitalData.Core.Exceptions; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Enums; +using EnvelopeGenerator.Application.Documents.Queries; +using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; +using EnvelopeGenerator.Domain.Entities; +using EnvelopeGenerator.Domain.Exceptions; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace EnvelopeGenerator.Application.PdfSigning; + +/// +/// +/// +public record SignPdfByEnvelopeReceiverCommand : IRequest +{ + /// + /// + /// + public int EnvelopeId { get; init; } + + /// + /// + /// + public int ReceiverId { get; init; } +} + +/// +/// +/// +public class SignPdfByEnvelopeReceiverCommandHandler(IRepository Repo, IMapper Mapper, ISender Sender) : IRequestHandler +{ + + /// + /// + /// + /// + /// + /// + /// + public async Task Handle(SignPdfByEnvelopeReceiverCommand request, CancellationToken cancel) + { + cancel.ThrowIfCancellationRequested(); + + // read elements + var elements = await Repo.Where(el => el.ReceiverId == request.ReceiverId && el.Document.EnvelopeId == request.EnvelopeId) + .Include(e => e.Receiver) + .ToListAsync(cancel) ?? throw new NotFoundException($"No elements found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}."); + var elementDtos = Mapper.Map>(elements) + .OrderBy(e => e.Page) + .ThenBy(e => e.Id) + .ToList(); + + // read secrets for phone number + var erSecrets = await Sender.Send(new ReadEnvelopeReceiverSecretQuery() + { + Envelope = new() { Id = request.EnvelopeId }, + Receiver = new() { Id = request.ReceiverId } + }, cancel) ?? throw new DataIntegrityException($"No secrets found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}."); + + // read document + var unsignedDoc = await Sender.Send(new ReadDocumentQuery(EnvelopeId: request.EnvelopeId), cancel); + var unsignedDocBytes = unsignedDoc.ByteData ?? throw new NotFoundException($"Document not found for Envelope ID {request.EnvelopeId} and Receiver ID {request.ReceiverId}."); + + Stream doc = new MemoryStream(unsignedDocBytes); + + // sign document for each element + foreach (var element in elementDtos) + { + var lastElement = element == elementDtos.Last(); + + var signedDoc = await Sender.Send(new SignPdfByElementCommand + { + Document = doc, + Element = element, + CertificationLevel = lastElement ? PdfSignatureCertificationLevel.NoChangesAllowed : PdfSignatureCertificationLevel.FillFormsAndAnnotate, + Phone = erSecrets.PhoneNumber + }, cancel); + + await doc.DisposeAsync(); + doc = signedDoc; + } + + return doc; + } +} diff --git a/EnvelopeGenerator.Domain/Exceptions/DataIntegrityException.cs b/EnvelopeGenerator.Domain/Exceptions/DataIntegrityException.cs new file mode 100644 index 00000000..0974286d --- /dev/null +++ b/EnvelopeGenerator.Domain/Exceptions/DataIntegrityException.cs @@ -0,0 +1,13 @@ +#if NETFRAMEWORK +using System; +#endif + +namespace EnvelopeGenerator.Domain.Exceptions +{ + public class DataIntegrityException : Exception + { + public DataIntegrityException(string message) : base(message) + { + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor index e668bbf9..0fee1078 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor @@ -56,7 +56,7 @@ title="@L("Complete", "Abschließen")" style="flex-shrink: 0;"> - + @L("Complete", "Abschließen") @@ -114,7 +114,7 @@
- +
@@ -169,7 +169,7 @@ SignatureCaptureDto? _sig; // ----- Submit / logout state ----- - bool _isLoggingOut = false; + bool _isLoggingOut = false; bool _submitConfirmVisible = false; string? _submitErrorMessage; @@ -262,7 +262,7 @@ } _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); - _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); + _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); // Burn signature image + info onto PDF via PdfSharp if (_sig is not null && _signatures.Count > 0) @@ -272,13 +272,13 @@ { PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, Landscape = false, - Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), + Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), }; var detail = new DetailBand(); report.Bands.Add(detail); detail.Controls.Add(new XRPdfContent { - Source = pdfBytes, + Source = pdfBytes, GenerateOwnPages = true, }); _report = report; @@ -317,11 +317,11 @@ // ----- PDF signature rendering ----- /// - /// Uses DevExpress PdfGraphics to burn the captured signature onto the PDF at each signature field. - /// Layout per field (input coordinates are top-left origin, units = PDF points): - /// [top 52%] signature image (aspect-ratio preserved) + /// Uses PdfSharp to burn the captured signature onto the PDF at each signature field. + /// Layout per field (top-left origin, Y down, units = PDF points): + /// [top 65%] signature image /// [separator line] - /// [bottom] FullName / Position (optional) / Place, Date + /// [bottom 35%] FullName (bold) / Position (optional) / Place, Date /// static byte[] DrawSignaturesOnPdf( byte[] pdfBytes, @@ -333,96 +333,100 @@ using var inputMs = new System.IO.MemoryStream(pdfBytes); using var outputMs = new System.IO.MemoryStream(); - using var processor = new DevExpress.Pdf.PdfDocumentProcessor(); - processor.LoadDocument(inputMs); - const double sigW = 1.77 * 72; - const double sigH = 1.96 * 72; - const double imgRatio = 0.52; - const double lineH = 11.5; - const double bgPad = 3.0; - const double padding = 3.0; + var document = PdfSharp.Pdf.IO.PdfReader.Open( + inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify); - using var bgBrush = new DevExpress.Drawing.DXSolidBrush(System.Drawing.Color.FromArgb(255, 255, 253, 240)); - using var linePen = new DevExpress.Drawing.DXPen(System.Drawing.Color.FromArgb(180, 100, 100, 120), 0.5f); - using var nameBrush = new DevExpress.Drawing.DXSolidBrush(System.Drawing.Color.FromArgb(255, 20, 20, 20)); - using var infoBrush = new DevExpress.Drawing.DXSolidBrush(System.Drawing.Color.FromArgb(255, 80, 80, 80)); - var fontBold = new DevExpress.Drawing.DXFont("Arial", 7.5f, DevExpress.Drawing.DXFontStyle.Bold); - var fontNormal = new DevExpress.Drawing.DXFont("Arial", 6.5f, DevExpress.Drawing.DXFontStyle.Regular); + const double sigW = 1.77 * 72; // 127.44 pt + const double sigH = 1.96 * 72; // 141.12 pt + const double imgRatio = 0.52; // top 52% = image + const double lineH = 11.5; // fixed row height matching font size (bold 7.5pt + normal 6.5pt) + const double bgPad = 3.0; // background box padding around content (pt) + + var black = PdfSharp.Drawing.XColor.FromArgb(255, 20, 20, 20); + var darkGray = PdfSharp.Drawing.XColor.FromArgb(255, 80, 80, 80); + var lineColor = PdfSharp.Drawing.XColor.FromArgb(180, 100, 100, 120); + + var bgColor = PdfSharp.Drawing.XColor.FromArgb(255, 255, 253, 240); + var bgBrush = new PdfSharp.Drawing.XSolidBrush(bgColor); + + var fontBold = new PdfSharp.Drawing.XFont("Arial", 7.5, PdfSharp.Drawing.XFontStyleEx.Bold); + var fontNormal = new PdfSharp.Drawing.XFont("Arial", 6.5, PdfSharp.Drawing.XFontStyleEx.Regular); + var linePen = new PdfSharp.Drawing.XPen(lineColor, 0.5); + + var fmtLeft = new PdfSharp.Drawing.XStringFormat + { + Alignment = PdfSharp.Drawing.XStringAlignment.Near, + LineAlignment = PdfSharp.Drawing.XLineAlignment.Near, + }; var date = DateTime.Now.ToString("dd.MM.yyyy"); foreach (var field in signatures) { - var pageIndex = field.Page - 1; - if (pageIndex < 0 || pageIndex >= processor.Document.Pages.Count) - continue; + int pageIndex = field.Page - 1; + if (pageIndex < 0 || pageIndex >= document.PageCount) continue; - var page = processor.Document.Pages[pageIndex]; - var pageHeight = page.CropBox.Height; + var page = document.Pages[pageIndex]; - var x = field.X; - var y = field.Y; + using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); - var imgH = sigH * imgRatio; - var lineY = y + imgH + 1.0; - var textY = lineY + 1.5; - var row1Y = textY; - var row2Y = row1Y + lineH; - var row3Y = string.IsNullOrWhiteSpace(sig.Position) ? row2Y : row2Y + lineH; - var contentBottom = row3Y + lineH; + double x = field.X; + double y = field.Y; - using var graphics = processor.CreateGraphics(); + // --- Calculate layout positions first (needed for bg rect) --- + double imgH = sigH * imgRatio; + double lineY = y + imgH + 1.0; // 1pt gap between image and separator + double textY = lineY + 1.5; // 1.5pt gap below separator line + double padding = 3; - var bgRect = ToPdfRect(x - bgPad, y - bgPad, sigW + (bgPad * 2), (contentBottom - y) + (bgPad * 2), pageHeight); - graphics.FillRectangle(bgBrush, bgRect); + // Row 1: FullName + double row1Y = textY; + // Row 2: Position (optional) + double row2Y = row1Y + lineH; + // Row 3: Place, Date — immediately after row2 regardless of position + double row3Y = !string.IsNullOrWhiteSpace(sig.Position) ? row2Y + lineH : row2Y; + double contentBottom = row3Y + lineH; - var imageArea = ToPdfRect(x, y, sigW, imgH, pageHeight); - var imageRect = FitImageContain(imageArea, imgBytes); - graphics.DrawImage(imgBytes, imageRect); + // --- Background rectangle sized to actual content (not full sigH) --- + var bgRect = new PdfSharp.Drawing.XRect( + x - bgPad, + y - bgPad, + sigW + bgPad * 2, + (contentBottom - y) + bgPad * 2); + gfx.DrawRectangle(bgBrush, bgRect); - var lineYPdf = ToPdfY(lineY, pageHeight); - graphics.DrawLine(linePen, (float)(x + 2), lineYPdf, (float)(x + sigW - 2), lineYPdf); + // --- Image area --- + var imgRect = new PdfSharp.Drawing.XRect(x, y, sigW, imgH); + using var imgStream = new System.IO.MemoryStream(imgBytes); + var xImg = PdfSharp.Drawing.XImage.FromStream(imgStream); + gfx.DrawImage(xImg, imgRect); - graphics.DrawString(sig.FullName, fontBold, nameBrush, ToPdfRect(x + padding, row1Y, sigW - (padding * 2), lineH, pageHeight)); + // --- Separator line --- + gfx.DrawLine(linePen, x + 2, lineY, x + sigW - 2, lineY); + // --- Text rows --- + // Row 1: FullName (bold) + var nameRect = new PdfSharp.Drawing.XRect(x + padding, row1Y, sigW - padding * 2, lineH); + gfx.DrawString(sig.FullName, fontBold, new PdfSharp.Drawing.XSolidBrush(black), nameRect, fmtLeft); + + // Row 2: Position (optional) if (!string.IsNullOrWhiteSpace(sig.Position)) { - graphics.DrawString(sig.Position, fontNormal, infoBrush, ToPdfRect(x + padding, row2Y, sigW - (padding * 2), lineH, pageHeight)); + var posRect = new PdfSharp.Drawing.XRect(x + padding, row2Y, sigW - padding * 2, lineH); + gfx.DrawString(sig.Position, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), posRect, fmtLeft); } - graphics.DrawString($"{sig.Place}, {date}", fontNormal, infoBrush, ToPdfRect(x + padding, row3Y, sigW - (padding * 2), lineH, pageHeight)); - - graphics.AddToPageForeground(page, 72, 72); + // Row 3: Place, Date + var placeDate = $"{sig.Place}, {date}"; + var dateRect = new PdfSharp.Drawing.XRect(x + padding, row3Y, sigW - padding * 2, lineH); + gfx.DrawString(placeDate, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), dateRect, fmtLeft); } - processor.SaveDocument(outputMs); + document.Save(outputMs); return outputMs.ToArray(); } - static System.Drawing.RectangleF FitImageContain(System.Drawing.RectangleF area, byte[] imageBytes) - { - using var imageStream = new System.IO.MemoryStream(imageBytes, writable: false); - using var image = DevExpress.Drawing.DXImage.FromStream(imageStream); - - if (image.Width <= 0 || image.Height <= 0) - return area; - - var scale = Math.Min(area.Width / image.Width, area.Height / image.Height); - var drawW = image.Width * scale; - var drawH = image.Height * scale; - var drawX = area.X + ((area.Width - drawW) / 2f); - var drawY = area.Y + ((area.Height - drawH) / 2f); - - return new System.Drawing.RectangleF(drawX, drawY, drawW, drawH); - } - - static System.Drawing.RectangleF ToPdfRect(double x, double y, double width, double height, double pageHeight) - => new((float)x, (float)(pageHeight - y - height), (float)width, (float)height); - - static float ToPdfY(double y, double pageHeight) - => (float)(pageHeight - y); - static byte[]? DataUrlToBytes(string? dataUrl) { if (string.IsNullOrWhiteSpace(dataUrl)) return null; diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs index d35bbb86..bd046d93 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs @@ -1,5 +1,9 @@ using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.Application.PdfSigning; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Extensions; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -8,11 +12,37 @@ namespace EnvelopeGenerator.Server.Controllers; /// /// /// -/// +/// +/// [ApiController] [Route("api/[controller]")] -public class PdfSignatureController(IPdfDigitalSignatureService signatureService) : ControllerBase +public class PdfSignatureController(IPdfDigitalSignatureService SignatureService, IMediator Mediator) : ControllerBase { + /// + /// Gets the document for the specified envelope key. + /// + /// + /// + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpPost("{envelopeKey}")] + public async Task SignDocument(string envelopeKey, CancellationToken cancel) + { + int envelopeId = User.EnvelopeId(); + int receiverId = User.ReceiverId(); + + var senderDoc = await Mediator.Send(new SignPdfByEnvelopeReceiverCommand + { + EnvelopeId = envelopeId, + ReceiverId = receiverId + }, cancel); + + senderDoc.Position = 0; + var fileName = Path.GetFileNameWithoutExtension(envelopeKey); + return File(senderDoc, "application/pdf", $"{fileName}-signed.pdf"); + } + + #region Test Endpoints /// /// /// @@ -57,7 +87,7 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService var output = new MemoryStream(); - await signatureService.SignPdfAsync(input, output, request.Options, signatureImage, cancel); + await SignatureService.SignPdfAsync(input, output, request.Options, signatureImage, cancel); output.Position = 0; var fileName = Path.GetFileNameWithoutExtension(request.File.FileName); @@ -76,7 +106,8 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService public async Task IsSigned([FromForm] CheckPdfSignatureForm request, CancellationToken cancel) { await using var input = request.File.OpenReadStream(); - var result = await signatureService.CheckPdfSignatureAsync(input, cancel); + var result = await SignatureService.CheckPdfSignatureAsync(input, cancel); return Ok(result); } + #endregion } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json index 51bf6274..59647559 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json @@ -290,5 +290,9 @@ "MarginTopRatio": -0.1 } ] + }, + "ESignParams": { + "ApplicationName": "signFLOW", + "DefaultReason": " Bestätigt" } }