Files
EnvelopeGenerator/EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFBurner.cs
TekH f078bafdde Refactor Jobs namespace and improve PDF handling
Refactored all EnvelopeGenerator.Jobs files to use the EnvelopeGenerator.Jobs namespace instead of EnvelopeGenerator.CommonServices.Jobs. Updated the .csproj to remove custom content and compile includes for the Jobs folder. Switched FinalizeDocumentJob to use dependency injection for PDFBurner, PDFMerger, and ReportCreator. Improved image annotation logic in PDFBurner for better placement and scaling, and refactored form field value rendering for conditional font styling. Aliased Document as LayoutDocument in ReportCreator to avoid ambiguity. Removed the obsolete Class1.cs file and made minor type safety improvements. These changes modernize the codebase and enhance maintainability.
2026-01-22 09:51:16 +01:00

263 lines
8.8 KiB
C#

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<PDFBurner> _logger;
private readonly PDFBurnerParams _pdfBurnerParams;
public PDFBurner() : this(NullLogger<PDFBurner>.Instance, new PDFBurnerParams())
{
}
public PDFBurner(ILogger<PDFBurner> logger, PDFBurnerParams? pdfBurnerParams = null)
{
_logger = logger;
_pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams();
}
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList<string> 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<string>())
{
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(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<string, Attachment>? 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<Annotation>? annotations { get; set; }
public Dictionary<string, Attachment>? attachments { get; set; }
public List<FormFieldValue>? formFieldValues { get; set; }
}
private sealed class Annotation
{
public string id { get; set; } = string.Empty;
public List<double> 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<List<List<float>>> 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
}