using System.Collections.Immutable; using System.Drawing; using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Infrastructure; using EnvelopeGenerator.PdfEditor; using EnvelopeGenerator.ServiceHost.Exceptions; using GdPicture14; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Newtonsoft.Json; namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument; //TODO: check if licence manager is needed as a dependency to /// /// /// /// /// /// /// /// public class PDFBurner(IOptions workerOptions, EGDbContext context, ILogger logger, LicenseManager licenseManager, AnnotationManager manager) { private readonly WorkerOptions.PDFBurnerOptions _options = workerOptions.Value.PdfBurner; public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, List instantJsonList, int envelopeId) { var envelope = context.Envelopes.FirstOrDefault(env => env.Id == envelopeId); if (envelope is null) { throw new BurnAnnotationException($"Envelope with Id {envelopeId} not found."); } if (envelope.ReadOnly) { return sourceBuffer; } var elements = context.DocumentReceiverElements .Where(sig => sig.Document.EnvelopeId == envelopeId) .Include(sig => sig.Annotations) .ToList(); return elements.Any() ? BurnElementAnnotsToPDF(sourceBuffer, elements) : BurnInstantJSONAnnotsToPDF(sourceBuffer, instantJsonList); } public byte[] BurnElementAnnotsToPDF(byte[] sourceBuffer, List elements) { using (var doc = Pdf.FromMemory(sourceBuffer)) { sourceBuffer = doc.Background(elements, 1.9500000000000002 * 0.93, 2.52 * 0.67).ExportStream().ToArray(); using var sourceStream = new MemoryStream(sourceBuffer); var result = manager.InitFromStream(sourceStream); if (result != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not open document for burning: [{result}]"); } var margin = 0.2; var inchFactor = 72d; var keys = new[] { "position", "city", "date" }; var unitYOffsets = 0.2; var yOffsetsOfFF = keys .Select((k, i) => new { Key = k, Value = unitYOffsets * i + 1 }) .ToDictionary(x => x.Key, x => x.Value); 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 ?? 0) - frameY * inchFactor; var frameXShift = (frame?.X ?? 0) - frameX * inchFactor; foreach (var annot in element.Annotations) { var yOffsetOfFF = yOffsetsOfFF.TryGetValue(annot.Name, out var offset) ? offset : 0; var y = frameY + yOffsetOfFF; if (annot.Type == AnnotationType.FormField) { AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value); } else if (annot.Type == AnnotationType.Image) { AddImageAnnotation( annot.X / inchFactor, annot.Name == "signature" ? (annot.Y - frameYShift) / inchFactor : y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value); } else if (annot.Type == AnnotationType.Ink) { AddInkAnnotation(element.Page, annot.Value); } } } using var newStream = new MemoryStream(); result = manager.SaveDocumentToPDF(newStream); if (result != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not save document to stream: [{result}]"); } manager.Close(); return newStream.ToArray(); } } public byte[] BurnInstantJSONAnnotsToPDF(byte[] sourceBuffer, List instantJsonList) { using var sourceStream = new MemoryStream(sourceBuffer); var result = manager.InitFromStream(sourceStream); if (result != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not open document for burning: [{result}]"); } foreach (var json in instantJsonList) { try { AddInstantJSONAnnotationToPDF(json); } catch (Exception ex) { logger.LogWarning("Error in AddInstantJSONAnnotationToPDF - oJson: "); logger.LogWarning(json); throw new BurnAnnotationException("Adding Annotation failed", ex); } } result = manager.BurnAnnotationsToPage(RemoveInitialAnnots: true, VectorMode: true); if (result != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not burn annotations to file: [{result}]"); } using var newStream = new MemoryStream(); result = manager.SaveDocumentToPDF(newStream); if (result != GdPictureStatus.OK) { throw new BurnAnnotationException($"Could not save document to stream: [{result}]"); } manager.Close(); return newStream.ToArray(); } private void AddInstantJSONAnnotationToPDF(string instantJson) { var annotationData = JsonConvert.DeserializeObject(instantJson); if (annotationData is null) { return; } annotationData.annotations.Reverse(); foreach (var annotation in annotationData.annotations) { logger.LogDebug("Adding AnnotationID: " + annotation.id); switch (annotation.type) { case AnnotationType.Image: AddImageAnnotation(annotation, annotationData.attachments); break; case AnnotationType.Ink: AddInkAnnotation(annotation); break; case AnnotationType.Widget: var formFieldValue = annotationData.formFieldValues.FirstOrDefault(fv => fv.name == annotation.id); if (formFieldValue is not null && !_options.IgnoredLabels.Contains(formFieldValue.value)) { AddFormFieldValue(annotation, formFieldValue); } break; } } } private void AddImageAnnotation(double x, double y, double width, double height, int page, string base64) { manager.SelectPage(page); manager.AddEmbeddedImageAnnotFromBase64(base64, (float) x, (float) y, (float) width, (float) height); } private void AddImageAnnotation(Annotation annotation, Dictionary attachments) { var attachment = attachments.Where(a => a.Key == annotation.imageAttachmentId).SingleOrDefault(); var bounds = annotation.bbox.Select(ToInches).ToList(); var x = bounds[0]; var y = bounds[1]; var width = bounds[2]; var height = bounds[3]; manager.SelectPage(annotation.pageIndex + 1); manager.AddEmbeddedImageAnnotFromBase64(attachment.Value.binary, (float) x, (float) y, (float) width, (float) height); } private void AddInkAnnotation(int page, string value) { var ink = JsonConvert.DeserializeObject(value); if (ink is null) { return; } var segments = ink.lines.points; var color = ColorTranslator.FromHtml(ink.strokeColor); manager.SelectPage(page); foreach (var segment in segments) { var points = segment.Select(ToPointF).ToArray(); manager.AddFreeHandAnnot(color, points); } } private void AddInkAnnotation(Annotation annotation) { var segments = annotation.lines.points; var color = ColorTranslator.FromHtml(annotation.strokeColor); manager.SelectPage(annotation.pageIndex + 1); foreach (var segment in segments) { var points = segment.Select(ToPointF).ToArray(); manager.AddFreeHandAnnot(color, points); } } private void AddFormFieldValue(double x, double y, double width, double height, int page, string value) { manager.SelectPage(page); var annot = manager.AddTextAnnot((float) x, (float) y, (float) width, (float) height, value); annot.FontName = _options.FontName; annot.FontSize = _options.FontSize; annot.FontStyle = _options.FontStyle; manager.SaveAnnotationsToPage(); } private void AddFormFieldValue(Annotation annotation, FormFieldValue formFieldValue) { var ffIndex = EGName.Index[annotation.egName]; var bounds = annotation.bbox.Select(ToInches).ToList(); var x = bounds[0]; var y = bounds[1] + _options.YOffset * ffIndex + _options.TopMargin; var width = bounds[2]; var height = bounds[3]; manager.SelectPage(annotation.pageIndex + 1); var annot = manager.AddTextAnnot((float) x, (float) y, (float) width, (float) height, formFieldValue.value); annot.FontName = _options.FontName; annot.FontSize = _options.FontSize; annot.FontStyle = _options.FontStyle; manager.SaveAnnotationsToPage(); } private static PointF ToPointF(List points) { var convertedPoints = points.Select(ToInches).ToList(); return new PointF(convertedPoints[0], convertedPoints[1]); } private static double ToInches(double value) => value / 72; private static float ToInches(float value) => value / 72; internal static class AnnotationType { public const string Image = "pspdfkit/image"; public const string Ink = "pspdfkit/ink"; public const string Widget = "pspdfkit/widget"; public const string FormField = "pspdfkit/form-field-value"; } internal class AnnotationData { public List annotations { get; set; } = new(); public IEnumerable> AnnotationsByReceiver => annotations .Where(annot => annot.hasStructuredID) .GroupBy(a => a.receiverId) .Select(g => g.ToList()); public IEnumerable> UnstructuredAnnotations => annotations .Where(annot => !annot.hasStructuredID) .GroupBy(a => a.receiverId) .Select(g => g.ToList()); public Dictionary attachments { get; set; } = new(); public List formFieldValues { get; set; } = new(); } internal class Annotation { private string? _id; public int envelopeId; public int receiverId; public int index; public string egName = EGName.NoName; public bool hasStructuredID; public bool isLabel { get { if (string.IsNullOrEmpty(egName)) { return false; } var parts = egName.Split('_'); return parts.Length > 1 && parts[1] == "label"; } } public string id { get => _id ?? string.Empty; set { _id = value; if (string.IsNullOrWhiteSpace(_id)) { throw new BurnAnnotationException("The identifier of annotation is null or empty."); } var parts = value.Split('#'); if (parts.Length != 4) { return; } if (!int.TryParse(parts[0], out envelopeId)) { throw new BurnAnnotationException($"The envelope ID of annotation is not integer. Id: {_id}"); } if (!int.TryParse(parts[1], out receiverId)) { throw new BurnAnnotationException($"The receiver ID of annotation is not integer. Id: {_id}"); } if (!int.TryParse(parts[2], out index)) { throw new BurnAnnotationException($"The index of annotation is not integer. Id: {_id}"); } egName = parts[3]; hasStructuredID = true; } } public List bbox { get; set; } = new(); public string type { get; set; } = string.Empty; public bool isSignature { get; set; } public string imageAttachmentId { get; set; } = string.Empty; public Lines lines { get; set; } = new(); public int pageIndex { get; set; } public string strokeColor { get; set; } = string.Empty; } internal class Ink { public Lines lines { get; set; } = new(); public string strokeColor { get; set; } = string.Empty; } public class EGName { public static readonly string NoName = Guid.NewGuid().ToString(); public static readonly string Seal = "signature"; public static readonly ImmutableDictionary Index = new Dictionary { { NoName, 0 }, { Seal, 0 }, { "position", 1 }, { "city", 2 }, { "date", 3 } }.ToImmutableDictionary(); } internal class Lines { public List>> points { get; set; } = new(); } internal class Attachment { public string binary { get; set; } = string.Empty; public string contentType { get; set; } = string.Empty; } internal class FormFieldValue { public string name { get; set; } = string.Empty; public string value { get; set; } = string.Empty; } }