From 232b3418940a10ea4d137121c535d25ad6688baa Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 19 Oct 2026 10:48:06 +0200 Subject: [PATCH] Refactor PDF signing logic and appearance handling Refactored `Pkcs11Options` by removing `DefaultCertificationLevel` and improving property documentation. Replaced `VisualSignature` in `PdfSignOptionsDto` with `Appearance` using the new `PdfSignatureAppearanceDto` class, which encapsulates signature appearance details, including font and color settings. Enhanced `PdfSignatureCertificationLevel` enum with more descriptive values and added XML documentation. Updated `IPdfDigitalSignatureService` to support optional signature images and removed redundant methods. Refactored `PdfDigitalSignatureService` to streamline appearance handling, validate inputs, and improve maintainability. Removed unused methods and added support for `PdfSignatureAppearanceDto`. Simplified `PdfSignatureController` by consolidating form properties into `PdfSignOptionsDto`. Improved code readability and flexibility while removing redundant logic. --- .../Common/Configurations/Pkcs11Options.cs | 5 - .../Common/Dto/PdfSignOptionsDto.cs | 2 +- .../Common/Dto/PdfSignatureAppearanceDto.cs | 61 ++++++ .../Dto/PdfVisualSignatureOptionsDto.cs | 20 -- .../Enums/PdfSignatureCertificationLevel.cs | 24 ++- .../Services/IPdfDigitalSignatureService.cs | 22 +- .../Helper/PdfCertificationLevelExtensions.cs | 16 ++ .../Services/PdfDigitalSignatureService.cs | 195 ++++++------------ .../Controllers/PdfSignatureController.cs | 80 +------ 9 files changed, 170 insertions(+), 255 deletions(-) create mode 100644 EnvelopeGenerator.Application/Common/Dto/PdfSignatureAppearanceDto.cs delete mode 100644 EnvelopeGenerator.Application/Common/Dto/PdfVisualSignatureOptionsDto.cs create mode 100644 EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs diff --git a/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs b/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs index 5b89a9f4..5215cc71 100644 --- a/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs +++ b/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs @@ -91,9 +91,4 @@ public sealed class Pkcs11Options /// true uses , false uses . /// public bool UseUtcSigningTime { get; set; } = true; - - /// - /// Optional global default certification level used when request-level options do not specify a value. - /// - public PdfSignatureCertificationLevel? DefaultCertificationLevel { get; set; } } diff --git a/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs b/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs index 9b9ccf60..70bf3ac5 100644 --- a/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs @@ -50,5 +50,5 @@ public sealed record PdfSignOptionsDto /// /// /// - public PdfVisualSignatureOptionsDto VisualSignature { get; set; } = null!; + public PdfSignatureAppearanceDto? Appearance { get; set; } } diff --git a/EnvelopeGenerator.Application/Common/Dto/PdfSignatureAppearanceDto.cs b/EnvelopeGenerator.Application/Common/Dto/PdfSignatureAppearanceDto.cs new file mode 100644 index 00000000..a3716d4e --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Dto/PdfSignatureAppearanceDto.cs @@ -0,0 +1,61 @@ +namespace EnvelopeGenerator.Application.Common.Dto; + +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +public sealed record PdfSignatureAppearanceDto( + int PageNumber, + double Left, + double Bottom, + double Width, + double Height, + bool ShowLabels = false, + bool ShowName = false, + bool ShowDate = false, + bool ShowReason = false, + bool ShowLocation = false, + bool ShowDistinguishedName = false, + string DateTimeFormat = "dd.MM.yyyy HH:mm:ss zzz") +{ + /// + /// + /// + /// + /// + /// + /// + /// + public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false) + { + /// + /// + /// + public RgbaColorDto Color { get; init; } = new(); + } + + /// + /// + /// + /// + /// + /// + public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0); + + /// + /// + /// + public FontDto Font { get; init; } = new(); +} diff --git a/EnvelopeGenerator.Application/Common/Dto/PdfVisualSignatureOptionsDto.cs b/EnvelopeGenerator.Application/Common/Dto/PdfVisualSignatureOptionsDto.cs deleted file mode 100644 index 4716c1bb..00000000 --- a/EnvelopeGenerator.Application/Common/Dto/PdfVisualSignatureOptionsDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EnvelopeGenerator.Application.Common.Dto; - -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -public sealed record PdfVisualSignatureOptionsDto( - byte[] ImageBytes, - int PageNumber, - double Left, - double Bottom, - double Width, - double Height, - bool ShowLabels = false); diff --git a/EnvelopeGenerator.Application/Common/Enums/PdfSignatureCertificationLevel.cs b/EnvelopeGenerator.Application/Common/Enums/PdfSignatureCertificationLevel.cs index f629465e..dc46493f 100644 --- a/EnvelopeGenerator.Application/Common/Enums/PdfSignatureCertificationLevel.cs +++ b/EnvelopeGenerator.Application/Common/Enums/PdfSignatureCertificationLevel.cs @@ -1,9 +1,27 @@ namespace EnvelopeGenerator.Application.Common.Enums; +/// +/// +/// public enum PdfSignatureCertificationLevel { + /// + /// All changes are allowed + /// NoCertification = 0, - NoChangesAllowed = 1, - FormFillingAndSigning = 2, - FormFillingAndAnnotations = 3 + + /// + /// No changes are allowed + /// + NoChangesAllowed, + + /// + /// Allows users to fill in forms and sign the document. + /// + FillForms, + + /// + /// Allows users to fill in forms, create, modify and delete annotations, and sign the document. + /// + FillFormsAndAnnotate } diff --git a/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs b/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs index cd875279..db8ab3c5 100644 --- a/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs +++ b/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs @@ -14,18 +14,9 @@ public interface IPdfDigitalSignatureService /// Source PDF content stream. /// Destination stream for signed PDF output. /// Optional signing options that map to PDF signature metadata fields. + /// Optional image stream for signature appearance. /// Cancellation token for cooperative cancellation. - Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, CancellationToken cancellationToken = default); - - /// - /// Applies multiple signatures to the provided PDF in sequence and returns - /// the final signed PDF document. - /// - /// Source PDF content stream. - /// Destination stream for signed PDF output. - /// Ordered signing options; one signature is applied per item. - /// Cancellation token for cooperative cancellation. - Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList optionsList, CancellationToken cancellationToken = default); + Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default); /// /// Checks whether the provided PDF contains signature field(s) and whether @@ -35,13 +26,4 @@ public interface IPdfDigitalSignatureService /// Cancellation token for cooperative cancellation. /// A structured result containing signature presence and per-signature details. Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default); - - /// - /// Extracts the original signed revision from a signed PDF. - /// For multi-revision files this returns the first signed revision bytes. - /// - /// Signed PDF content stream. - /// Destination stream for original revision output. - /// Cancellation token for cooperative cancellation. - Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default); } diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs b/EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs new file mode 100644 index 00000000..c6c24250 --- /dev/null +++ b/EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs @@ -0,0 +1,16 @@ +using DevExpress.Pdf; +using EnvelopeGenerator.Application.Common.Enums; + +namespace EnvelopeGenerator.Infrastructure.Crypt.Helper; + +public static class PdfCertificationLevelExtensions +{ + public static PdfCertificationLevel ToDevExpress(this PdfSignatureCertificationLevel value) => value switch + { + PdfSignatureCertificationLevel.NoChangesAllowed => PdfCertificationLevel.NoChangesAllowed, + PdfSignatureCertificationLevel.FillForms => PdfCertificationLevel.FillForms, + PdfSignatureCertificationLevel.NoCertification => PdfCertificationLevel.NoCertification, + PdfSignatureCertificationLevel.FillFormsAndAnnotate => PdfCertificationLevel.FillFormsAndAnnotate, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), + }; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs index 8aa933b0..844fa1e8 100644 --- a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs +++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs @@ -1,13 +1,15 @@ -using EnvelopeGenerator.Application.Common.Configurations; -using EnvelopeGenerator.Application.Common.Dto; -using EnvelopeGenerator.Application.Common.Enums; -using EnvelopeGenerator.Application.Common.Interfaces.Services; using DevExpress.Office.DigitalSignatures; using DevExpress.Pdf; +using DigitalData.Core.Exceptions; +using EnvelopeGenerator.Application.Common.Configurations; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.Infrastructure.Crypt.Helper; using Microsoft.Extensions.Options; using System.Formats.Asn1; -using System.Security.Cryptography.Pkcs; +using System.IO; using System.Security.Cryptography; +using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; @@ -18,7 +20,7 @@ public sealed class PdfDigitalSignatureService(IOptions options) { private static readonly Regex SignatureReasonMetaRegex = new(@"\|\s*SFMETA:v1;doc=(?[0-9a-fA-F-]{36});sha256=(?[A-Fa-f0-9]{64})$", RegexOptions.CultureInvariant | RegexOptions.Compiled); - public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto request, CancellationToken cancellationToken = default) + public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto request, Stream? image = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -32,48 +34,66 @@ public sealed class PdfDigitalSignatureService(IOptions options) var pkcs11Options = options.Value; - var uuid = ResolveUuid(request.UUID); - var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex); + var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, request.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(signaturePageNumber) - { - Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}" - }; - var left = request.VisualSignature!.Left; - var bottom = request.VisualSignature.Bottom; - signatureFieldInfo.SignatureBounds = new PdfRectangle(left, bottom, left + request.VisualSignature.Width, bottom + request.VisualSignature.Height); - - var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo) + var builder = request.Appearance is null ? new PdfDeferredSignatureBuilder(signerInfo) : new (signerInfo, new PdfSignatureFieldInfo(request.Appearance.PageNumber) { - Name = request.Name ?? pkcs11Options.CertificateLabel, - Reason = reasonWithMeta, - Location = request.Location, - ContactInfo = BuildContactInfo(request.Email, request.Phone), - SigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now, - ApplicationName = pkcs11Options.ApplicationName, - CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel) - ?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel) - ?? PdfCertificationLevel.NoCertification - }; - - builder.SetSignatureAppearance(new PdfSignatureAppearance - { - AppearanceType = PdfSignatureAppearanceType.Name, - ShowLabels = request.VisualSignature.ShowLabels, - ShowName = true, - ShowDate = false, - ShowReason = false, - ShowLocation = false, - ShowDistinguishedName = false + Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}", + SignatureBounds = new PdfRectangle(request.Appearance.Left, request.Appearance.Bottom, request.Appearance.Left + request.Appearance.Width, request.Appearance.Bottom + request.Appearance.Height) }); - builder.SetImageData(request.VisualSignature.ImageBytes); - + + builder.Name = request.Name; + builder.Reason = reasonWithMeta; + builder.Location = request.Location; + builder.ContactInfo = BuildContactInfo(request.Email, request.Phone); + builder.SigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now; + builder.ApplicationName = pkcs11Options.ApplicationName; + builder.CertificationLevel = request.CertificationLevel.ToDevExpress(); + + if (request.Appearance is not null) + { + var fColor = request.Appearance.Font.Color; + var font = new PdfSignatureFontSettings(request.Appearance.Font.FamilyName) + { + Size = request.Appearance.Font.Size, + Color = new PdfRGBColor(fColor.R / 255.0, fColor.G / 255.0, fColor.B / 255.0), + Bold = false, + Italic = true + }; + + var appearance = new PdfSignatureAppearance + { + ShowLabels = request.Appearance.ShowLabels, + ShowName = request.Appearance.ShowName, + ShowDate = request.Appearance.ShowDate, + ShowReason = request.Appearance.ShowReason, + ShowLocation = request.Appearance.ShowLocation, + ShowDistinguishedName = request.Appearance.ShowDistinguishedName, + DateTimeFormat = request.Appearance.DateTimeFormat, + SignatureDetailsFont = font, + SignatureNameFont = font, + AppearanceType = PdfSignatureAppearanceType.Image, + }; + + + if (image is not null) + using (var memoryStream = new MemoryStream()) + { + image.CopyTo(memoryStream); + var bytes = memoryStream.ToArray(); + appearance.SetBackgroundImageData(bytes); + } + + builder.SetSignatureAppearance(appearance); + } + else if (image is not null) + throw new BadRequestException("Signature appearance is not specified, but signature image is provided."); + var deferredSigner = signer.SignDeferred(builder); var externalSigner = new Pkcs11ExternalSigner(pkcs11Options); deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid)); @@ -85,49 +105,7 @@ public sealed class PdfDigitalSignatureService(IOptions options) } } - public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList optionsList, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (optionsList.Count == 0) - { - await inputPdf.CopyToAsync(outputPdf, cancellationToken); - return; - } - - var (currentInput, ownsCurrentInput) = await EnsureSeekableInputAsync(inputPdf, cancellationToken); - - try - { - for (var i = 0; i < optionsList.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - var isLast = i == optionsList.Count - 1; - - if (isLast) - { - await SignPdfAsync(currentInput, outputPdf, optionsList[i], cancellationToken); - continue; - } - - var intermediateOutput = new MemoryStream(); - await SignPdfAsync(currentInput, intermediateOutput, optionsList[i], cancellationToken); - - if (ownsCurrentInput) - await currentInput.DisposeAsync(); - - intermediateOutput.Position = 0; - currentInput = intermediateOutput; - ownsCurrentInput = true; - } - } - finally - { - if (ownsCurrentInput) - await currentInput.DisposeAsync(); - } - } - + //TODO: verify and optimize (remove memory stream etc.) the check algortihm public async Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -135,7 +113,7 @@ public sealed class PdfDigitalSignatureService(IOptions options) var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken); using var pdfStream = new MemoryStream(pdfBytes); - using var signer = new PdfDocumentSigner(pdfStream); + using var signer = new PdfDocumentSigner(inputPdf); var signatures = signer.GetSignatureInfo(); var byteRanges = ParseByteRanges(pdfBytes); @@ -235,31 +213,6 @@ public sealed class PdfDigitalSignatureService(IOptions options) Signatures: items); } - public async Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken); - - var byteRanges = ParseByteRanges(pdfBytes); - if (byteRanges.Count == 0) - { - await outputPdf.WriteAsync(pdfBytes, cancellationToken); - return; - } - - var revisionLength = byteRanges - .Select(GetSignedRevisionLength) - .Where(x => x > 0) - .DefaultIfEmpty(pdfBytes.LongLength) - .Min(); - - if (revisionLength > pdfBytes.LongLength) - throw new InvalidOperationException("Invalid ByteRange in signed PDF."); - - await outputPdf.WriteAsync(pdfBytes.AsMemory(0, (int)revisionLength), cancellationToken); - } - private static async Task<(Stream Stream, bool OwnsStream)> EnsureSeekableInputAsync(Stream input, CancellationToken cancellationToken) { if (input.CanSeek) @@ -398,11 +351,6 @@ public sealed class PdfDigitalSignatureService(IOptions options) return new PdfSignaturePrincipalDto(commonName, organization, country, dn); } - private static PdfCertificationLevel? ToDevExpressCertificationLevel(PdfSignatureCertificationLevel? value) - { - return value is null ? null : (PdfCertificationLevel)(int)value.Value; - } - private static string? ToColonSerial(string? raw) { if (string.IsNullOrWhiteSpace(raw)) @@ -544,25 +492,6 @@ public sealed class PdfDigitalSignatureService(IOptions options) return start + length; } - private static string ResolveUuid(string? uuid) - { - return Guid.TryParse(uuid, out var parsed) - ? parsed.ToString() - : 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 string BuildReasonWithMeta(string? businessReason, string uuid, string sourceHashHex) { var trimmedReason = string.IsNullOrWhiteSpace(businessReason) ? null : businessReason.Trim(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs index f4c1b634..d35bbb86 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs @@ -1,5 +1,4 @@ using EnvelopeGenerator.Application.Common.Dto; -using EnvelopeGenerator.Application.Common.Enums; using EnvelopeGenerator.Application.Common.Interfaces.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -33,59 +32,12 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService /// /// /// - public required IFormFile SignatureImageFile { get; init; } + public required IFormFile SignatureImage { get; init; } /// /// /// - public required string? UUID { get; init; } - - /// - /// - /// - public required string? Name { get; init; } - - /// - /// - /// - public required string? Reason { get; init; } - - /// - /// - /// - public required string? Location { get; init; } - - /// - /// - /// - public required string? Email { get; init; } - - /// - /// - /// - public required string? Phone { get; init; } - - /// - /// - /// - public required VSignature Signature { get; init; } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - public record VSignature( - int PageNumber = 1, - double Left = 100, - double Bottom = 100, - double Width = 220, - double Height = 80, - bool ShowLabels = true); + public PdfSignOptionsDto Options { get; init; } = new PdfSignOptionsDto(); } /// @@ -99,32 +51,14 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService [Consumes("multipart/form-data")] public async Task Sign([FromForm] SignPdfForm request, CancellationToken cancel) { - await using var signatureImageInput = request.SignatureImageFile.OpenReadStream(); - using var signatureImageBuffer = new MemoryStream(); - await signatureImageInput.CopyToAsync(signatureImageBuffer, cancel); - - var signOptions = new PdfSignOptionsDto - { - UUID = request.UUID, - Name = request.Name, - Reason = request.Reason, - Location = request.Location, - Email = request.Email, - Phone = request.Phone, - 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 signatureImage = request.SignatureImage.OpenReadStream(); await using var input = request.File.OpenReadStream(); + var output = new MemoryStream(); - await signatureService.SignPdfAsync(input, output, signOptions, cancel); + + await signatureService.SignPdfAsync(input, output, request.Options, signatureImage, cancel); + output.Position = 0; var fileName = Path.GetFileNameWithoutExtension(request.File.FileName); return File(output, "application/pdf", $"{fileName}-signed.pdf");