using System.Collections.Generic; using System.Drawing; using System.Linq; using iText.IO.Image; using iText.Kernel.Colors; using iText.Kernel.Pdf; using iText.Kernel.Pdf.Canvas; using iText.Layout; using iText.Layout.Element; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions; using LayoutImage = iText.Layout.Element.Image; namespace EnvelopeGenerator.Jobs.FinalizeDocument; public class PDFBurner { private readonly ILogger _logger; private readonly PDFBurnerParams _pdfBurnerParams; public PDFBurner() : this(NullLogger.Instance, new PDFBurnerParams()) { } public PDFBurner(ILogger logger, PDFBurnerParams? pdfBurnerParams = null) { _logger = logger; _pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams(); } public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList instantJsonList, int envelopeId) { if (sourceBuffer is null || sourceBuffer.Length == 0) { throw new BurnAnnotationException("Source document is empty"); } try { using var inputStream = new MemoryStream(sourceBuffer); using var outputStream = new MemoryStream(); using var reader = new PdfReader(inputStream); using var writer = new PdfWriter(outputStream); using var pdf = new PdfDocument(reader, writer); foreach (var json in instantJsonList ?? Enumerable.Empty()) { if (string.IsNullOrWhiteSpace(json)) { continue; } var annotationData = JsonConvert.DeserializeObject(json); if (annotationData?.annotations is null) { continue; } annotationData.annotations.Reverse(); foreach (var annotation in annotationData.annotations) { try { switch (annotation.type) { case AnnotationType.Image: AddImageAnnotation(pdf, annotation, annotationData.attachments); break; case AnnotationType.Ink: AddInkAnnotation(pdf, annotation); break; case AnnotationType.Widget: var formFieldValue = annotationData.formFieldValues?.FirstOrDefault(fv => fv.name == annotation.id); if (formFieldValue is not null && !_pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value)) { AddFormFieldValue(pdf, annotation, formFieldValue.value); } break; } } catch (Exception ex) { _logger.LogWarning(ex, "Error applying annotation {AnnotationId} on envelope {EnvelopeId}", annotation.id, envelopeId); throw new BurnAnnotationException("Adding annotation failed", ex); } } } pdf.Close(); return outputStream.ToArray(); } catch (BurnAnnotationException) { throw; } catch (Exception ex) { _logger.LogError(ex, "Failed to burn annotations for envelope {EnvelopeId}", envelopeId); throw new BurnAnnotationException("Annotations could not be burned", ex); } } private void AddImageAnnotation(PdfDocument pdf, Annotation annotation, Dictionary? attachments) { if (attachments is null || string.IsNullOrWhiteSpace(annotation.imageAttachmentId) || !attachments.TryGetValue(annotation.imageAttachmentId, out var attachment)) { return; } var page = pdf.GetPage(annotation.pageIndex + 1); var bounds = annotation.bbox.Select(ToInches).ToList(); var x = (float)bounds[0]; var y = (float)bounds[1]; var width = (float)bounds[2]; var height = (float)bounds[3]; var imageBytes = Convert.FromBase64String(attachment.binary); var imageData = ImageDataFactory.Create(imageBytes); var image = new LayoutImage(imageData) .ScaleAbsolute(width, height) .SetFixedPosition(annotation.pageIndex + 1, x, y); using var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize()); canvas.Add(image); } private void AddInkAnnotation(PdfDocument pdf, Annotation annotation) { if (annotation.lines?.points is null) { return; } var page = pdf.GetPage(annotation.pageIndex + 1); var canvas = new PdfCanvas(page); var color = ParseColor(annotation.strokeColor); canvas.SetStrokeColor(color); canvas.SetLineWidth(1); foreach (var segment in annotation.lines.points) { var first = true; foreach (var point in segment) { var (px, py) = (ToInches(point[0]), ToInches(point[1])); if (first) { canvas.MoveTo(px, py); first = false; } else { canvas.LineTo(px, py); } } canvas.Stroke(); } } private void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value) { var bounds = annotation.bbox.Select(ToInches).ToList(); var x = (float)bounds[0]; var y = (float)bounds[1]; var width = (float)bounds[2]; var height = (float)bounds[3]; var page = pdf.GetPage(annotation.pageIndex + 1); var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize()); var paragraph = new Paragraph(value) .SetFontSize(_pdfBurnerParams.FontSize) .SetFontColor(ColorConstants.BLACK) .SetFontFamily(_pdfBurnerParams.FontName); if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic)) { paragraph.SetItalic(); } if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold)) { paragraph.SetBold(); } canvas.ShowTextAligned(paragraph, x + (float)_pdfBurnerParams.TopMargin, y + (float)_pdfBurnerParams.YOffset, annotation.pageIndex + 1, iText.Layout.Properties.TextAlignment.LEFT, iText.Layout.Properties.VerticalAlignment.TOP, 0); } private static DeviceRgb ParseColor(string? color) { if (string.IsNullOrWhiteSpace(color)) { return new DeviceRgb(0, 0, 0); } try { var drawingColor = ColorTranslator.FromHtml(color); return new DeviceRgb(drawingColor.R, drawingColor.G, drawingColor.B); } catch { return new DeviceRgb(0, 0, 0); } } private static double ToInches(double value) => value / 72d; private static double ToInches(float value) => value / 72d; #region Model private static class AnnotationType { public const string Image = "pspdfkit/image"; public const string Ink = "pspdfkit/ink"; public const string Widget = "pspdfkit/widget"; } private sealed class AnnotationData { public List? annotations { get; set; } public Dictionary? attachments { get; set; } public List? formFieldValues { get; set; } } private sealed class Annotation { public string id { get; set; } = string.Empty; public List bbox { get; set; } = new(); public string type { get; set; } = string.Empty; public string imageAttachmentId { get; set; } = string.Empty; public Lines? lines { get; set; } public int pageIndex { get; set; } public string strokeColor { get; set; } = string.Empty; public string egName { get; set; } = string.Empty; } private sealed class Lines { public List>> points { get; set; } = new(); } private sealed class Attachment { public string binary { get; set; } = string.Empty; public string contentType { get; set; } = string.Empty; } private sealed class FormFieldValue { public string name { get; set; } = string.Empty; public string value { get; set; } = string.Empty; } #endregion }