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:
@@ -12,4 +12,5 @@ public sealed record PdfSignOptionsDto
|
||||
public string? Phone { get; set; }
|
||||
public string? ApplicationName { get; set; }
|
||||
public PdfSignatureCertificationLevel? CertificationLevel { get; set; }
|
||||
public PdfVisualSignatureOptionsDto? VisualSignature { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
public sealed record PdfVisualSignatureOptionsDto(
|
||||
byte[] ImageBytes,
|
||||
int PageNumber,
|
||||
double Left,
|
||||
double Bottom,
|
||||
double Width,
|
||||
double Height,
|
||||
bool ShowLabels = false);
|
||||
@@ -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();
|
||||
|
||||
@@ -6,38 +6,102 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="signatureService"></param>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PdfSignatureController(IPdfDigitalSignatureService signatureService) : ControllerBase
|
||||
{
|
||||
public sealed class SignPdfForm
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class CheckPdfSignatureForm
|
||||
{
|
||||
public IFormFile? File { get; set; }
|
||||
public string? UUID { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public virtual required IFormFile File { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CheckPdfSignatureForm
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public sealed class SignPdfForm : CheckPdfSignatureForm
|
||||
{
|
||||
public IFormFile? File { get; set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required IFormFile SignatureImageFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string? UUID { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string? Location { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string? Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required string? Phone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required VSignature Signature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="PageNumber"></param>
|
||||
/// <param name="Left"></param>
|
||||
/// <param name="Bottom"></param>
|
||||
/// <param name="Width"></param>
|
||||
/// <param name="Height"></param>
|
||||
/// <param name="ShowLabels"></param>
|
||||
public record VSignature(
|
||||
int PageNumber = 1,
|
||||
double Left = 100,
|
||||
double Bottom = 100,
|
||||
double Width = 220,
|
||||
double Height = 80,
|
||||
bool ShowLabels = true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("sign")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<IActionResult> Sign([FromForm] SignPdfForm request, CancellationToken cancel)
|
||||
{
|
||||
var file = request.File;
|
||||
|
||||
if (file is null || file.Length == 0)
|
||||
return BadRequest("PDF file is required.");
|
||||
|
||||
if (!file.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
return BadRequest("Only PDF files are supported.");
|
||||
await using var signatureImageInput = request.SignatureImageFile.OpenReadStream();
|
||||
using var signatureImageBuffer = new MemoryStream();
|
||||
await signatureImageInput.CopyToAsync(signatureImageBuffer, cancel);
|
||||
|
||||
var signOptions = new PdfSignOptionsDto
|
||||
{
|
||||
@@ -47,31 +111,37 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService
|
||||
Location = request.Location,
|
||||
Email = request.Email,
|
||||
Phone = request.Phone,
|
||||
CertificationLevel = PdfSignatureCertificationLevel.FormFillingAndAnnotations
|
||||
CertificationLevel = PdfSignatureCertificationLevel.FormFillingAndAnnotations,
|
||||
VisualSignature = new PdfVisualSignatureOptionsDto(
|
||||
ImageBytes: signatureImageBuffer.ToArray(),
|
||||
PageNumber: request.Signature.PageNumber,
|
||||
Left: request.Signature.Left,
|
||||
Bottom: request.Signature.Bottom,
|
||||
Width: request.Signature.Width,
|
||||
Height: request.Signature.Height,
|
||||
ShowLabels: request.Signature.ShowLabels)
|
||||
};
|
||||
|
||||
await using var input = file.OpenReadStream();
|
||||
await using var output = new MemoryStream();
|
||||
await using var input = request.File.OpenReadStream();
|
||||
var output = new MemoryStream();
|
||||
await signatureService.SignPdfAsync(input, output, signOptions, cancel);
|
||||
output.Position = 0;
|
||||
var fileName = Path.GetFileNameWithoutExtension(file.FileName);
|
||||
var fileName = Path.GetFileNameWithoutExtension(request.File.FileName);
|
||||
return File(output, "application/pdf", $"{fileName}-signed.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("is-signed")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<IActionResult> IsSigned([FromForm] CheckPdfSignatureForm request, CancellationToken cancel)
|
||||
{
|
||||
var file = request.File;
|
||||
|
||||
if (file is null || file.Length == 0)
|
||||
return BadRequest("PDF file is required.");
|
||||
|
||||
if (!file.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
return BadRequest("Only PDF files are supported.");
|
||||
|
||||
await using var input = file.OpenReadStream();
|
||||
await using var input = request.File.OpenReadStream();
|
||||
var result = await signatureService.CheckPdfSignatureAsync(input, cancel);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user