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.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Configurations;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ESignParams
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string DefaultReason { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -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.
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||
/// </summary>
|
||||
@@ -28,6 +30,11 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||
/// </summary>
|
||||
public int ReceiverId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public virtual ReceiverDto? Receiver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the element.
|
||||
/// </summary>
|
||||
|
||||
@@ -30,12 +30,12 @@ public sealed record PdfSignOptionsDto
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Email { get; set; } = null!;
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Phone { get; set; } = null!;
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -51,6 +51,7 @@ public static class DependencyInjection
|
||||
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
||||
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
|
||||
|
||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||
|
||||
@@ -21,7 +21,24 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
/// </remarks>
|
||||
public record ReadEnvelopeReceiverSecretQuery
|
||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
|
||||
IRequest<EnvelopeReceiverSecretDto?>;
|
||||
IRequest<EnvelopeReceiverSecretDto?>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public ReadEnvelopeReceiverSecretQuery() { }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeId"></param>
|
||||
/// <param name="receiverId"></param>
|
||||
public ReadEnvelopeReceiverSecretQuery(int envelopeId, int receiverId)
|
||||
{
|
||||
Envelope.Id = envelopeId;
|
||||
Receiver.Id = receiverId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
|
||||
|
||||
@@ -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<Stream>
|
||||
///
|
||||
/// </summary>
|
||||
public int? ElementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom document
|
||||
/// </summary>
|
||||
public Stream? Document { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public PdfSignatureCertificationLevel CertificationLevel { get; init; } = PdfSignatureCertificationLevel.NoChangesAllowed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="signService"></param>
|
||||
public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer) : IRequestHandler<SignPdfByElementCommand, Stream>
|
||||
/// <param name="imgRenderer"></param>
|
||||
/// <param name="options"></param>
|
||||
public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService signService, IPdfPageImageRenderer imgRenderer, IOptionsMonitor<ESignParams> options) : IRequestHandler<SignPdfByElementCommand, Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -37,49 +63,71 @@ public class SignPdfCommandHandler(ISender sender, IPdfDigitalSignatureService s
|
||||
/// <returns></returns>
|
||||
public async Task<Stream> 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}");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record SignPdfByEnvelopeReceiverCommand : IRequest<Stream>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int ReceiverId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class SignPdfByEnvelopeReceiverCommandHandler(IRepository<DocReceiverElement> Repo, IMapper Mapper, ISender Sender) : IRequestHandler<SignPdfByEnvelopeReceiverCommand, Stream>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task<Stream> 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<List<DocReceiverElementDto>>(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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Exceptions
|
||||
{
|
||||
public class DataIntegrityException : Exception
|
||||
{
|
||||
public DataIntegrityException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
title="@L("Complete", "Abschließen")"
|
||||
style="flex-shrink: 0;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span class="pdf-toolbar__btn-text">@L("Complete", "Abschließen")</span>
|
||||
</button>
|
||||
@@ -114,7 +114,7 @@
|
||||
<div style="display: flex; align-items: flex-start; gap: 1rem; padding: 0.5rem 0;">
|
||||
<div style="flex-shrink: 0; width: 40px; height: 40px; background: #d1fae5; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -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 -----
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="signatureService"></param>
|
||||
/// <param name="SignatureService"></param>
|
||||
/// <param name="Mediator"></param>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PdfSignatureController(IPdfDigitalSignatureService signatureService) : ControllerBase
|
||||
public class PdfSignatureController(IPdfDigitalSignatureService SignatureService, IMediator Mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the document for the specified envelope key.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("{envelopeKey}")]
|
||||
public async Task<IActionResult> 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
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -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<IActionResult> 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
|
||||
}
|
||||
|
||||
@@ -290,5 +290,9 @@
|
||||
"MarginTopRatio": -0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
"ESignParams": {
|
||||
"ApplicationName": "signFLOW",
|
||||
"DefaultReason": " Bestätigt"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user