Add support for visual PDF signatures

Enhanced `PdfSignOptionsDto` with a `VisualSignature` property and introduced the `PdfVisualSignatureOptionsDto` class for configuring visual signature options. Updated `PdfDigitalSignatureService` to handle visual signatures, including page number resolution, signature bounds calculation, and appearance customization.

Refactored `PdfSignatureController` to support visual signatures by extending `SignPdfForm` with `SignatureImageFile` and `VSignature` properties. Improved validation for page numbers and signature dimensions. Streamlined `Sign` and `IsSigned` endpoints for better clarity and reduced redundancy.

Added XML documentation for improved API clarity and performed general code cleanup to enhance maintainability.
This commit is contained in:
2026-10-16 10:10:04 +02:00
parent 04cea1ca4b
commit 198eabcbae
4 changed files with 163 additions and 31 deletions

View File

@@ -38,14 +38,19 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex);
using var signer = new PdfDocumentSigner(workingInput);
var signaturePageNumber = ResolveSignaturePageNumber(request.VisualSignature?.PageNumber, signer.PageCount);
var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256);
var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator);
var signatureFieldInfo = new PdfSignatureFieldInfo(1)
var signatureFieldInfo = new PdfSignatureFieldInfo(signaturePageNumber)
{
Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"
};
var signatureBounds = CreateSignatureBounds(request.VisualSignature);
if (signatureBounds is not null)
signatureFieldInfo.SignatureBounds = signatureBounds;
var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo)
{
Name = request.Name ?? pkcs11Options.CertificateLabel,
@@ -59,6 +64,21 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
?? PdfCertificationLevel.NoCertification
};
if(request.VisualSignature is PdfVisualSignatureOptionsDto visualSignature)
{
builder.SetSignatureAppearance(new PdfSignatureAppearance
{
AppearanceType = PdfSignatureAppearanceType.Name,
ShowLabels = visualSignature.ShowLabels,
ShowName = true,
ShowDate = false,
ShowReason = false,
ShowLocation = false,
ShowDistinguishedName = false
});
builder.SetImageData(visualSignature.ImageBytes);
}
var deferredSigner = signer.SignDeferred(builder);
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid));
@@ -535,6 +555,37 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
: Guid.NewGuid().ToString();
}
private static int ResolveSignaturePageNumber(int? requestedPageNumber, int pageCount)
{
var pageNumber = requestedPageNumber ?? 1;
if (pageNumber < 1)
throw new ArgumentOutOfRangeException(nameof(requestedPageNumber), "SignaturePageNumber must be greater than or equal to 1.");
if (pageCount > 0 && pageNumber > pageCount)
throw new ArgumentOutOfRangeException(nameof(requestedPageNumber), $"SignaturePageNumber {pageNumber} exceeds page count {pageCount}.");
return pageNumber;
}
private static PdfRectangle? CreateSignatureBounds(PdfVisualSignatureOptionsDto? visualOptions)
{
if (visualOptions is null)
return null;
var left = visualOptions.Left;
var bottom = visualOptions.Bottom;
var width = visualOptions.Width;
var height = visualOptions.Height;
if (left < 0 || bottom < 0)
throw new ArgumentOutOfRangeException(nameof(visualOptions), "VisualSignature.Left and VisualSignature.Bottom cannot be negative.");
if (width <= 0 || height <= 0)
throw new ArgumentOutOfRangeException(nameof(visualOptions), "VisualSignature.Width and VisualSignature.Height must be greater than zero.");
return new PdfRectangle(left, bottom, left + width, bottom + height);
}
private static string BuildReasonWithMeta(string? businessReason, string uuid, string sourceHashHex)
{
var trimmedReason = string.IsNullOrWhiteSpace(businessReason) ? null : businessReason.Trim();