using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Exceptions; using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Dto.PSPDFKitInstant; using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Exceptions; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities; using GdPicture14; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; namespace EnvelopeGenerator.Application.Pdf; /// /// /// public record BurnPdfCommand(int? EnvelopeId = null, string? EnvelopeUuid = null) : IRequest; /// /// /// public static class BurnPdfCommandExtensions { /// /// /// /// /// /// /// public static Task BurnPdf(this ISender sender, int envelopeId, CancellationToken cancel = default) => sender.Send(new BurnPdfCommand(EnvelopeId: envelopeId), cancel); /// /// /// /// /// /// /// public static Task BurnPdf(this ISender sender, string envelopeUuid, CancellationToken cancel = default) => sender.Send(new BurnPdfCommand(EnvelopeUuid: envelopeUuid), cancel); } /// /// /// public class BurnPdfCommandHandler : IRequestHandler { private readonly PDFBurnerParams _options; private readonly AnnotationManager _manager; private readonly ILogger _logger; private readonly IRepository _envRepo; private readonly IRepository _docStatusRepo; /// /// /// /// /// /// /// /// public BurnPdfCommandHandler(IOptions pdfBurnerParams, AnnotationManager manager, ILogger logger, IRepository envRepo, IRepository docStatusRepo) { _options = pdfBurnerParams.Value; _manager = manager; _docStatusRepo = docStatusRepo; _logger = logger; _envRepo = envRepo; } /// /// /// /// /// /// /// public async Task Handle(BurnPdfCommand request, CancellationToken cancel) { var envQuery = request.EnvelopeId is not null ? _envRepo.Where(env => env.Id == request.EnvelopeId) : request.EnvelopeUuid is not null ? _envRepo.Where(env => env.Uuid == request.EnvelopeUuid) : throw new BadRequestException("Request validation failed: Either Envelope Id or Envelope Uuid must be provided."); var envelope = await envQuery .Include(env => env.Documents!).ThenInclude(doc => doc.Elements!).ThenInclude(element => element.Annotations) .FirstOrDefaultAsync(cancel) ?? throw new BadRequestException($"Envelope could not be found. Request details:\n" + request.ToJson(Format.Json.ForDiagnostics)); var doc = envelope.Documents?.FirstOrDefault() ?? throw new NotFoundException($"Document could not be located within the specified envelope. Request details:\n" + request.ToJson(Format.Json.ForDiagnostics)); if (doc.ByteData is null) throw new InvalidOperationException($"Document byte data is missing, indicating a potential data integrity issue. Request details:\n" + request.ToJson(Format.Json.ForDiagnostics)); return doc.Elements?.SelectMany(e => e.Annotations ?? Enumerable.Empty()).Where(annot => annot is not null).Any() ?? false ? BurnElementAnnotsToPDF(doc.ByteData, doc.Elements) : BurnInstantJSONAnnotsToPDF(doc.ByteData, await _docStatusRepo .Where(status => status.EnvelopeId == envelope.Id) .Select(status => status.Value) .ToListAsync(cancel)); } private byte[] BurnElementAnnotsToPDF(byte[] pSourceBuffer, List elements) { // Add background using var doc = PdfEditor.Pdf.FromMemory(pSourceBuffer); // TODO: take the length from the largest y pSourceBuffer = doc.Background(elements, 1.9500000000000002 * 0.93, 2.52 * 0.67) .ExportStream() .ToArray(); GdPictureStatus oResult; using var oSourceStream = new MemoryStream(pSourceBuffer); // Open PDF oResult = _manager.InitFromStream(oSourceStream); if (oResult != GdPictureStatus.OK) throw new BurnAnnotationException($"Could not open document for burning: [{oResult}]"); // Imported from background (add to configuration) var margin = 0.2; var inchFactor = 72; // Y offset of form fields var keys = new[] { "position", "city", "date" }; // add to configuration var unitYOffsets = 0.2; var yOffsetsOfFF = keys .Select((k, i) => new { Key = k, Value = unitYOffsets * i + 1 }) .ToDictionary(x => x.Key, x => x.Value); // Add annotations foreach (var element in elements) { var frameX = element.Left - 0.7 - margin; var frame = element.Annotations?.FirstOrDefault(a => a.Name == "frame"); var frameY = element.Top - 0.5 - margin; var frameYShift = (frame!.Y ?? default) - frameY * inchFactor; var frameXShift = (frame.X ?? default) - frameX * inchFactor; foreach (var annot in element.Annotations!) { if (!yOffsetsOfFF.TryGetValue(annot.Name, out var yOffsetofFF)) yOffsetofFF = 0; var y = frameY + yOffsetofFF; if (annot.Type == AnnotationType.PSPDFKit.FormField) { _manager.AddFormFieldValue( (annot.X ?? default) / inchFactor, y, (annot.Width ?? default) / inchFactor, (annot.Height ?? default) / inchFactor, element.Page, annot.Value, _options ); } else if (annot.Type == AnnotationType.PSPDFKit.Image) { _manager.AddImageAnnotation( (annot.X ?? default) / inchFactor, annot.Name == "signature" ? ((annot.Y ?? default) - frameYShift) / inchFactor : y, (annot.Width ?? default) / inchFactor, (annot.Height ?? default) / inchFactor, element.Page, annot.Value ); } else if (annot.Type == AnnotationType.PSPDFKit.Ink) { _manager.AddInkAnnotation(element.Page, annot.Value); } } } // Save PDF using var oNewStream = new MemoryStream(); oResult = _manager.SaveDocumentToPDF(oNewStream); if (oResult != GdPictureStatus.OK) throw new BurnAnnotationException($"Could not save document to stream: [{oResult}]"); _manager.Close(); return oNewStream.ToArray(); } private byte[] BurnInstantJSONAnnotsToPDF(byte[] pSourceBuffer, List pInstantJSONList) { GdPictureStatus oResult; using var oSourceStream = new MemoryStream(pSourceBuffer); // Open PDF oResult = _manager.InitFromStream(oSourceStream); if (oResult != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not open document for burning: [{oResult}]"); } // Add annotation to PDF foreach (var oJSON in pInstantJSONList) { try { if (oJSON is string json) AddInstantJsonAnnotationToPdf(json); } catch (Exception ex) { _logger.LogWarning("Error in AddInstantJSONAnnotationToPDF - oJson: "); _logger.LogWarning(oJSON); throw new BurnAnnotationException("Adding Annotation failed", ex); } } oResult = _manager.BurnAnnotationsToPage(RemoveInitialAnnots: true, VectorMode: true); if (oResult != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not burn annotations to file: [{oResult}]"); } // Save PDF using var oNewStream = new MemoryStream(); oResult = _manager.SaveDocumentToPDF(oNewStream); if (oResult != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not save document to stream: [{oResult}]"); } _manager.Close(); return oNewStream.ToArray(); } private void AddInstantJsonAnnotationToPdf(string instantJson) { var annotationData = JsonConvert.DeserializeObject(instantJson); annotationData?.Annotations?.Reverse(); if (annotationData?.Annotations is null) return; foreach (var annotation in annotationData.Annotations) { _logger.LogDebug("Adding AnnotationID: {id}", annotation.Id); switch (annotation.Type) { case AnnotationType.PSPDFKit.Image: if (annotationData?.Attachments is not null) _manager.AddImageAnnotation(annotation, annotationData.Attachments); break; case AnnotationType.PSPDFKit.Ink: _manager.AddInkAnnotation(annotation); break; case AnnotationType.PSPDFKit.Widget: // Add form field values var formFieldValue = annotationData?.FormFieldValues? .FirstOrDefault(fv => fv.Name == annotation.Id); if (formFieldValue != null && !_options.IgnoredLabels.Contains(formFieldValue.Value)) { _manager.AddFormFieldValue(annotation, formFieldValue, _options); } break; } } } }