feat(FinalizeDocument): aus CommonJobs kopiert, mit einfachen Fehlerbehebungen unter Verwendung von Copilot

- Programmiersprache von VSC zu C# geändert
 - Framework von .NET Framework zu .NET geändert
This commit is contained in:
2026-02-23 17:12:25 +01:00
parent 41cca7fa64
commit c93c32307a
21 changed files with 1538 additions and 0 deletions

View File

@@ -0,0 +1,445 @@
using System.Collections.Immutable;
using System.Drawing;
using System.IO;
using DigitalData.Modules.Base;
using DigitalData.Modules.Logging;
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;
public class PDFBurner : BaseClass
{
private readonly AnnotationManager _manager;
private readonly LicenseManager _licenseManager;
private readonly DbContextOptions<EGDbContext> _dbContextOptions;
private readonly PDFBurnerParams _pdfBurnerParams;
public PDFBurner(LogConfig logConfig, string gdPictureLicenseKey, PDFBurnerParams pdfBurnerParams, string connectionString) : base(logConfig)
{
_licenseManager = new LicenseManager();
_licenseManager.RegisterKEY(gdPictureLicenseKey);
_manager = new AnnotationManager();
_pdfBurnerParams = pdfBurnerParams;
_dbContextOptions = new DbContextOptionsBuilder<EGDbContext>()
.UseSqlServer(connectionString)
.EnableDetailedErrors()
.EnableSensitiveDataLogging()
.Options;
}
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, List<string> instantJsonList, int envelopeId)
{
using var context = new EGDbContext(_dbContextOptions, Options.Create(new DbTriggerParams()));
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<Signature> 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<string> 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.Warn("Error in AddInstantJSONAnnotationToPDF - oJson: ");
Logger.Warn(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<AnnotationData>(instantJson);
if (annotationData is null)
{
return;
}
annotationData.annotations.Reverse();
foreach (var annotation in annotationData.annotations)
{
Logger.Debug("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 && !_pdfBurnerParams.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<string, Attachment> 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<Ink>(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 = _pdfBurnerParams.FontName;
annot.FontSize = _pdfBurnerParams.FontSize;
annot.FontStyle = _pdfBurnerParams.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] + _pdfBurnerParams.YOffset * ffIndex + _pdfBurnerParams.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 = _pdfBurnerParams.FontName;
annot.FontSize = _pdfBurnerParams.FontSize;
annot.FontStyle = _pdfBurnerParams.FontStyle;
_manager.SaveAnnotationsToPage();
}
private static PointF ToPointF(List<float> 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<Annotation> annotations { get; set; } = new();
public IEnumerable<List<Annotation>> AnnotationsByReceiver => annotations
.Where(annot => annot.hasStructuredID)
.GroupBy(a => a.receiverId)
.Select(g => g.ToList());
public IEnumerable<List<Annotation>> UnstructuredAnnotations => annotations
.Where(annot => !annot.hasStructuredID)
.GroupBy(a => a.receiverId)
.Select(g => g.ToList());
public Dictionary<string, Attachment> attachments { get; set; } = new();
public List<FormFieldValue> 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<double> 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<string, int> Index = new Dictionary<string, int>
{
{ NoName, 0 },
{ Seal, 0 },
{ "position", 1 },
{ "city", 2 },
{ "date", 3 }
}.ToImmutableDictionary();
}
internal class Lines
{
public List<List<List<float>>> 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;
}
}

View File

@@ -0,0 +1,18 @@
using System.Drawing;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class PDFBurnerParams
{
public List<string> IgnoredLabels { get; set; } = new() { "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" };
public double TopMargin { get; set; } = 0.1;
public double YOffset { get; set; } = -0.3;
public string FontName { get; set; } = "Arial";
public int FontSize { get; set; } = 8;
public FontStyle FontStyle { get; set; } = FontStyle.Italic;
}

View File

@@ -0,0 +1,65 @@
using System.IO;
using DigitalData.Modules.Base;
using DigitalData.Modules.Logging;
using EnvelopeGenerator.ServiceHost.Exceptions;
using GdPicture14;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class PDFMerger : BaseClass
{
private readonly AnnotationManager _manager;
private readonly LicenseManager _licenseManager;
private const bool AllowRasterization = true;
private const bool AllowVectorization = true;
private readonly PdfConversionConformance _pdfaConformanceLevel = PdfConversionConformance.PDF_A_1b;
public PDFMerger(LogConfig logConfig, string gdPictureLicenseKey) : base(logConfig)
{
_licenseManager = new LicenseManager();
_licenseManager.RegisterKEY(gdPictureLicenseKey);
_manager = new AnnotationManager();
}
public byte[] MergeDocuments(byte[] document, byte[] report)
{
using var documentStream = new MemoryStream(document);
using var reportStream = new MemoryStream(report);
using var finalStream = new MemoryStream();
using var documentPdf = new GdPicturePDF();
using var reportPdf = new GdPicturePDF();
documentPdf.LoadFromStream(documentStream, true);
var status = documentPdf.GetStat();
if (status != GdPictureStatus.OK)
{
throw new MergeDocumentException($"Document could not be loaded: {status}");
}
reportPdf.LoadFromStream(reportStream, true);
status = reportPdf.GetStat();
if (status != GdPictureStatus.OK)
{
throw new MergeDocumentException($"Report could not be loaded: {status}");
}
var mergedPdf = documentPdf.Merge2Documents(documentPdf, reportPdf);
status = mergedPdf.GetStat();
if (status != GdPictureStatus.OK)
{
throw new MergeDocumentException($"Documents could not be merged: {status}");
}
mergedPdf.ConvertToPDFA(finalStream, _pdfaConformanceLevel, AllowVectorization, AllowRasterization);
status = documentPdf.GetStat();
if (status != GdPictureStatus.OK)
{
throw new MergeDocumentException($"Document could not be converted to PDF/A: {status}");
}
return finalStream.ToArray();
}
}

View File

@@ -0,0 +1,107 @@
using System.Data;
using System.IO;
using DigitalData.Modules.Base;
using DigitalData.Modules.Logging;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.ServiceHost.Exceptions;
using EnvelopeGenerator.ServiceHost.Jobs;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class ReportCreator : BaseClass
{
private Envelope? _envelope;
private readonly ReportModel _reportModel;
public ReportCreator(LogConfig logConfig, State state) : base(logConfig)
{
_reportModel = new ReportModel(state);
}
public byte[] CreateReport(Envelope envelope)
{
try
{
Logger.Debug("Loading report data..");
var table = _reportModel.List(envelope.Id);
var items = GetReportSource(table);
_envelope = envelope;
if (items.Count == 0)
{
throw new CreateReportException("No report data found!");
}
Logger.Debug("Creating report with [{0}] items..", items.Count);
var buffer = DoCreateReport(items);
Logger.Debug("Report created!");
return buffer;
}
catch (Exception ex)
{
Logger.Error(ex);
throw new CreateReportException("Could not prepare report data!", ex);
}
}
private List<ReportItem> GetReportSource(DataTable dataTable)
{
Logger.Debug("Preparing report data");
return dataTable.Rows
.Cast<DataRow>()
.Select(ToReportItem)
.OrderByDescending(r => r.ItemDate)
.ToList();
}
private byte[] DoCreateReport(List<ReportItem> reportItems)
{
var items = reportItems.Select(MergeEnvelope).ToList();
var source = new ReportSource { Items = items };
var report = new rptEnvelopeHistory { DataSource = source, DataMember = "Items" };
Logger.Debug("Creating report in memory..");
report.CreateDocument();
Logger.Debug("Exporting report to stream..");
using var stream = new MemoryStream();
report.ExportToPdf(stream);
Logger.Debug("Writing report to buffer..");
return stream.ToArray();
}
private ReportItem MergeEnvelope(ReportItem item)
{
if (item.Envelope is null)
{
item.Envelope = _envelope;
}
return item;
}
private ReportItem ToReportItem(DataRow row)
{
try
{
return new ReportItem
{
EnvelopeId = row.ItemEx("ENVELOPE_ID", 0),
EnvelopeTitle = row.ItemEx("HEAD_TITLE", string.Empty),
EnvelopeSubject = row.ItemEx("HEAD_SUBJECT", string.Empty),
ItemDate = row.ItemEx("POS_WHEN", DateTime.MinValue),
ItemStatus = (EnvelopeStatus)row.ItemEx("POS_STATUS", 0),
ItemUserReference = row.ItemEx("POS_WHO", string.Empty)
};
}
catch (Exception ex)
{
Logger.Error(ex);
throw new CreateReportException("Could not read data from database!", ex);
}
}
}

View File

@@ -0,0 +1,19 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class ReportItem
{
public Envelope? Envelope { get; set; }
public int EnvelopeId { get; set; }
public string EnvelopeTitle { get; set; } = string.Empty;
public string EnvelopeSubject { get; set; } = string.Empty;
public EnvelopeStatus ItemStatus { get; set; }
public string ItemStatusTranslated => ItemStatus.ToString();
public string ItemUserReference { get; set; } = string.Empty;
public DateTime ItemDate { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class ReportSource
{
public List<ReportItem> Items { get; set; } = new();
}

View File

@@ -0,0 +1,18 @@
using System.IO;
namespace EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
public class rptEnvelopeHistory
{
public object? DataSource { get; set; }
public string? DataMember { get; set; }
public void CreateDocument()
{
}
public void ExportToPdf(Stream stream)
{
stream.Write(Array.Empty<byte>(), 0, 0);
}
}