Compare commits

..

1 Commits

Author SHA1 Message Date
b76b59f37b Migrate from iText to GdPicture for PDF processing
Replaced iText with GdPicture for PDF manipulation, including:
- Added `Width` and `Height` properties to `IDocReceiverElement`.
- Updated `Pdf.cs` to use GdPicture's API for drawing and exporting.
- Introduced GdPicture licensing management with `EnsureInitialized`.
- Refactored `Background` method to align with GdPicture's coordinate system.
- Simplified resource management in `Dispose` and `DisposeAsync`.

Updated project files:
- Modified target frameworks in `EnvelopeGenerator.PdfEditor.csproj` to remove `net7.0` and `net9.0`, and added `net8.0-windows`.
- Replaced iText and BouncyCastle dependencies with GdPicture packages.
- Added `Microsoft.Bcl.AsyncInterfaces` to `EnvelopeGenerator.Web.csproj`.

Performed general cleanup and refactoring to remove unused iText code.
2026-08-24 11:09:19 +02:00
56 changed files with 183 additions and 1543 deletions

View File

@@ -1,7 +1,6 @@
Imports System.IO Imports System.IO
Imports DigitalData.Modules.Database Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Logging Imports DigitalData.Modules.Logging
Imports EnvelopeGenerator.CommonServices
Imports EnvelopeGenerator.CommonServices.Jobs Imports EnvelopeGenerator.CommonServices.Jobs
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
Imports GdPicture14 Imports GdPicture14
@@ -146,23 +145,25 @@ Public Class frmFinalizePDF
log.AppendLine($"Document bytes: {oBuffer.Length}") log.AppendLine($"Document bytes: {oBuffer.Length}")
' 3. Check what BurnAnnotsToPDF will do internally ' 3. Check what BurnAnnotsToPDF will do internally
Dim oEnvelopeEntity As Envelope = Nothing
Using scope = Factory.Shared.ScopeFactory.CreateScope() Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)() Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault() Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If oEnvelopeEntity Is Nothing Then If envelope Is Nothing Then
log.AppendLine("ERROR: Envelope not found in EF Core!") log.AppendLine("ERROR: Envelope not found in EF Core!")
txtResult.Text = log.ToString() txtResult.Text = log.ToString()
Return Return
End If End If
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}") log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}") log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
If envelope.ReadOnly Then
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
txtResult.Text = log.ToString()
Return
End If
If oEnvelopeEntity.ReadOnly Then
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
Else
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)() Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
Dim elements = sigRepo _ Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _ .Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
@@ -185,52 +186,23 @@ Public Class frmFinalizePDF
Else Else
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)") log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
End If End If
End If
End Using End Using
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged) ' 4. Actually call BurnAnnotsToPDF (same as Service)
log.AppendLine("") log.AppendLine("")
log.AppendLine("Calling BurnAnnotsToPDF...") log.AppendLine("Calling BurnAnnotsToPDF...")
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId) Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}") log.AppendLine($"Result bytes: {oNewBuffer.Length}")
log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}") log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
' 5. Build State and load Envelope domain model for ReportCreator ' 5. Write output
log.AppendLine("")
log.AppendLine("Loading envelope domain model for report...")
Dim oState As New State() With {
.Database = Database,
.LogConfig = LogConfig
}
Dim oEnvelopeModel As New EnvelopeModel(oState)
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
If oEnvelope Is Nothing Then
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
txtResult.Text = log.ToString()
Return
End If
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
' 6. Create signing certificate report
log.AppendLine("Creating signing certificate report...")
Dim oReportCreator As New ReportCreator(LogConfig, oState)
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
' 7. Merge burned document + report (last page)
log.AppendLine("Merging documents...")
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
' 8. Save and open
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf") Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
File.WriteAllBytes(oMergedPath, oMergedBuffer) File.WriteAllBytes(oNewPath, oNewBuffer)
log.AppendLine($"Output: {oMergedPath}") log.AppendLine($"Output: {oNewPath}")
txtResult.Text = log.ToString() txtResult.Text = log.ToString()
Process.Start(oMergedPath) Process.Start(oNewPath)
Catch ex As Exception Catch ex As Exception
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}" txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"

View File

@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
Dim y = frameY + yOffsetofFF Dim y = frameY + yOffsetofFF
If annot.Type = AnnotationType.FormField Then If annot.Type = AnnotationType.FormField Then
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name) AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
ElseIf annot.Type = AnnotationType.Image Then ElseIf annot.Type = AnnotationType.Image Then
AddImageAnnotation( AddImageAnnotation(
annot.X / inchFactor, annot.X / inchFactor,
@@ -269,14 +269,11 @@ Namespace Jobs.FinalizeDocument
Next Next
End Sub End Sub
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "") Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
Manager.SelectPage(page) Manager.SelectPage(page)
' Truncate if needed
Dim displayValue As String = TruncateText(value, egName)
' Add the text annotation ' Add the text annotation
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue) Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
' Set the font properties ' Set the font properties
ant.FontName = _pdfBurnerParams.FontName ant.FontName = _pdfBurnerParams.FontName
@@ -297,9 +294,8 @@ Namespace Jobs.FinalizeDocument
Dim oHeight = oBounds.Item(3) Dim oHeight = oBounds.Item(3)
Manager.SelectPage(pAnnotation.pageIndex + 1) Manager.SelectPage(pAnnotation.pageIndex + 1)
' Add the text annotation (truncate if needed) ' Add the text annotation
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName) Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
' Set the font properties ' Set the font properties
ant.FontName = _pdfBurnerParams.FontName ant.FontName = _pdfBurnerParams.FontName
@@ -310,48 +306,6 @@ Namespace Jobs.FinalizeDocument
#End Region #End Region
#Region "Helpers" #Region "Helpers"
''' <summary>
''' Truncates a field value so its rendered width fits within the configured max width (inches).
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
''' Appends TruncationSuffix when the value is cut.
''' </summary>
Private Function TruncateText(value As String, fieldEgName As String) As String
If String.IsNullOrEmpty(value) Then Return value
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
Dim maxWidthInches As Single = 0
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
If maxWidthInches <= 0 Then Return value
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
Using bmp As New Bitmap(1, 1)
Using g As Graphics = Graphics.FromImage(bmp)
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
' Already fits — no truncation needed
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
' Binary search for the longest prefix that fits (with suffix)
Dim lo As Integer = 0
Dim hi As Integer = value.Length
While lo < hi
Dim mid As Integer = (lo + hi + 1) \ 2
Dim candidate As String = value.Substring(0, mid) & suffix
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
lo = mid
Else
hi = mid - 1
End If
End While
Return value.Substring(0, lo) & suffix
End Using
End Using
End Function
Private Function ToPointF(pPoints As List(Of Single)) As PointF Private Function ToPointF(pPoints As List(Of Single)) As PointF
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList() Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
Return New PointF(oPoints.Item(0), oPoints.Item(1)) Return New PointF(oPoints.Item(0), oPoints.Item(1))

View File

@@ -12,24 +12,5 @@ Namespace Jobs.FinalizeDocument
Public Property FontSize As Integer = 8 Public Property FontSize As Integer = 8
Public Property FontStyle As FontStyle = FontStyle.Italic Public Property FontStyle As FontStyle = FontStyle.Italic
''' <summary>
''' The suffix appended to a truncated field value. Defaults to "..".
''' Can be changed to any string (e.g. "…", "...", "").
''' </summary>
Public Property TruncationSuffix As String = ".."
''' <summary>
''' Per-field maximum render width in inches.
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
''' Set a value to 0 to disable truncation for that field.
''' </summary>
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
{"position", 1.41F},
{"city", 1.41F},
{"date", 2.0F}
}
End Class End Class
End Namespace End Namespace

View File

@@ -1,6 +1,5 @@
Imports EnvelopeGenerator.Domain.Constants Imports EnvelopeGenerator.Domain.Constants
Imports EnvelopeGenerator.Domain.Entities Imports EnvelopeGenerator.Domain.Entities
Imports EnvelopeGenerator.Domain.Interfaces
Public Class ReportItem Public Class ReportItem
@@ -12,15 +11,8 @@ Public Class ReportItem
Public Property ItemStatus As EnvelopeStatus Public Property ItemStatus As EnvelopeStatus
Public ReadOnly Property ItemStatusTranslated As String Public ReadOnly Property ItemStatusTranslated As String
Get Get
Dim oStatusKey = ItemStatus.ToString() Dim oStatus = ItemStatus.ToString()
Return My.Resources.Model.ResourceManager.GetString(oStatus)
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
Envelope IsNot Nothing AndAlso
Envelope.IsReadAndConfirm() Then
oStatusKey = "DocumentSignedRaC"
End If
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
End Get End Get
End Property End Property

View File

@@ -168,9 +168,6 @@
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Document signed</value> <value>Document signed</value>
</data> </data>
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Read and confirmed</value>
</data>
<data name="Draft" xml:space="preserve"> <data name="Draft" xml:space="preserve">
<value>Draft</value> <value>Draft</value>
</data> </data>

View File

@@ -168,9 +168,6 @@
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Dokument unterzeichnet</value> <value>Dokument unterzeichnet</value>
</data> </data>
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Gelesen und bestätigt</value>
</data>
<data name="Draft" xml:space="preserve"> <data name="Draft" xml:space="preserve">
<value>Entwurf</value> <value>Entwurf</value>
</data> </data>

View File

@@ -7,5 +7,9 @@
double X { get; set; } double X { get; set; }
double Y { get; set; } double Y { get; set; }
double Width { get; set; }
double Height { get; set; }
} }
} }

View File

@@ -213,15 +213,6 @@ namespace My.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Gelesen und bestätigt.
/// </summary>
public static string DocumentSignedRaC {
get {
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Entwurf. /// Looks up a localized string similar to Entwurf.
/// </summary> /// </summary>

View File

@@ -168,9 +168,6 @@
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Dokument unterzeichnet</value> <value>Dokument unterzeichnet</value>
</data> </data>
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Gelesen und bestätigt</value>
</data>
<data name="Draft" xml:space="preserve"> <data name="Draft" xml:space="preserve">
<value>Entwurf</value> <value>Entwurf</value>
</data> </data>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net462;net8.0-windows</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net462'"> <PropertyGroup Condition="'$(TargetFramework)' == 'net462'">
@@ -17,9 +17,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" /> <PackageReference Include="GdPicture" Version="14.4.7" />
<PackageReference Include="itext" Version="8.0.5" /> <PackageReference Include="GdPicture.runtimes.windows" Version="14.4.7" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,8 +1,5 @@
using iText.Kernel.Pdf; using EnvelopeGenerator.Domain.Interfaces;
using iText.Kernel.Pdf.Canvas; using GdPicture14;
using EnvelopeGenerator.Domain.Interfaces;
using iText.Kernel.Colors;
using DigitalData.Core.Abstractions.Interfaces;
#if NETFRAMEWORK #if NETFRAMEWORK
using System; using System;
@@ -15,8 +12,26 @@ namespace EnvelopeGenerator.PdfEditor
{ {
public static class Pdf public static class Pdf
{ {
private const string LicenseKey = "kG1Qf9PwmqgR8aDmIW2zI_ebj48RzqAJegRxcystEmkbTGQqfkNBdFOXIb6C_A00Ra8zZkrHdfjqzOPXK7kgkF2YDhvrqKfqh4WDug2vOt0qO31IommzkANSuLjZ4zmraoubyEVd25rE3veQ2h_j7tGIoH_LyIHmy24GaXsxdG0yCzIBMdiLbMMMDwcPY-809KeZ83Grv76OVhFvcbBWyYc251vou1N-kGg5_ZlHDgfWoY85gTLRxafjD3KS_i9ARW4BMiy36y8n7UP2jN8kGRnW_04ubpFtfjJqvtsrP_J9D0x7bqV8xtVtT5JI6dpKsVTiMgDCrIcoFSo5gCC1fw9oUopX4TDCkBQttO4-WHBlOeq9dG5Yb0otonVmJKaQA2tP6sMR-lZDs3ql_WI9t91yPWgpssrJUxSHDd27_LMTH_owJIqkF3NOJd9mYQuAv22oNKFYbH8e41pVKb8cT33Y9CgcQ_sy6YDA5PTuIRi67mjKge_nD9rd0IN213Ir9M_EFWqg9e4haWzIdHXQUo0md70kVhPX4UIH_BKJnxEEnFfoFRNMh77bB0N4jkcBEHPl-ghOERv8dOztf4vCnNpzzWvcLD2cqWIm6THy8XGGq9h4hp8aEreRleSMwv9QQAC7mjLwhQ1rBYkpUHlpTjhTLnMwHknl6HH0Z6zzmsgkRKVyfquv94Pd7QbQfZrRka0ss_48pf9p8hAywEn81Q==";
private static bool _initialized = false;
private static readonly object _initLock = new object();
internal static void EnsureInitialized()
{
if (_initialized) return;
lock (_initLock)
{
if (_initialized) return;
var licenseManager = new LicenseManager();
licenseManager.RegisterKEY(LicenseKey);
_initialized = true;
}
}
public static Pdf<MemoryStream, MemoryStream> FromMemory(byte[] documentBytes) public static Pdf<MemoryStream, MemoryStream> FromMemory(byte[] documentBytes)
{ {
EnsureInitialized();
return new Pdf<MemoryStream, MemoryStream>(new MemoryStream(documentBytes), new MemoryStream()); return new Pdf<MemoryStream, MemoryStream>(new MemoryStream(documentBytes), new MemoryStream());
} }
@@ -26,6 +41,7 @@ namespace EnvelopeGenerator.PdfEditor
#endif #endif
outputStream = null) outputStream = null)
{ {
EnsureInitialized();
return new Pdf<MemoryStream, MemoryStream>(stream, outputStream ?? new MemoryStream(), disposeInputStream: false, disposeOutputStream: outputStream is null); return new Pdf<MemoryStream, MemoryStream>(stream, outputStream ?? new MemoryStream(), disposeInputStream: false, disposeOutputStream: outputStream is null);
} }
} }
@@ -34,115 +50,114 @@ namespace EnvelopeGenerator.PdfEditor
where TInputStream : Stream where TInputStream : Stream
where TOutputStream : Stream where TOutputStream : Stream
{ {
private readonly PdfDocument _doc;
private readonly TInputStream _inputStream; private readonly TInputStream _inputStream;
private readonly TOutputStream _outputStream; private readonly TOutputStream _outputStream;
private readonly PdfReader _reader;
private readonly PdfWriter _writer;
private readonly bool _disposeInputStream; private readonly bool _disposeInputStream;
private readonly bool _disposeOutputStream; private readonly bool _disposeOutputStream;
// Holds the current PDF bytes; replaced after each Background() call
private byte[] _pdfBytes;
public Pdf(TInputStream inputStream, TOutputStream outputStream, bool disposeInputStream = true, bool disposeOutputStream = true) public Pdf(TInputStream inputStream, TOutputStream outputStream, bool disposeInputStream = true, bool disposeOutputStream = true)
{ {
_inputStream = inputStream; _inputStream = inputStream;
_outputStream = outputStream; _outputStream = outputStream;
_reader = new PdfReader(inputStream);
_writer = new PdfWriter(outputStream);
_doc = new PdfDocument(_reader, _writer);
_disposeInputStream = disposeInputStream; _disposeInputStream = disposeInputStream;
_disposeOutputStream = disposeOutputStream; _disposeOutputStream = disposeOutputStream;
_pdfBytes = ReadAllBytes(inputStream);
}
private static byte[] ReadAllBytes(Stream stream)
{
if (stream is MemoryStream ms)
return ms.ToArray();
using (var buf = new MemoryStream())
{
stream.CopyTo(buf);
return buf.ToArray();
}
} }
/// <summary> /// <summary>
/// Gets the output stream containing the edited PDF document. /// Flushes the current PDF bytes to the output stream and returns it.
/// After calling this, the instance should not be modified further.
/// </summary> /// </summary>
/// <remarks>
/// Accessing this property will close the underlying <see cref="PdfDocument"/> to ensure
/// all changes are flushed to the stream. After accessing this property, the PDF document
/// can no longer be modified using this <see cref="Pdf{TInputStream, TOutputStream}"/> instance.
/// </remarks>
/// <returns>
/// The <typeparamref name="TOutputStream"/> instance that contains the updated PDF bytes.
/// </returns>
public TOutputStream ExportStream() public TOutputStream ExportStream()
{ {
_doc.Close(); _outputStream.SetLength(0);
_outputStream.Position = 0;
_outputStream.Write(_pdfBytes, 0, _pdfBytes.Length);
_outputStream.Position = 0;
return _outputStream; return _outputStream;
} }
#region Edit methods /// <summary>
public Pdf<TInputStream, TOutputStream> Document(Action<PdfDocument> edit) /// Draws the signature field background rectangles onto the PDF pages.
{ ///
edit(_doc); /// Coordinate system: GdPicture SetOrigin(PdfOriginTopLeft) + SetMeasurementUnit(PdfMeasurementUnitInch)
return this; /// matches the Form app's GdPicture annotation coordinate system exactly.
} /// DB values (X, Y in inches from top-left) are used directly — zero conversion.
/// </summary>
public Pdf<TInputStream, TOutputStream> Page(int pageIndex, Action<PdfPage> design) public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthInch = 1.9500000000000002, double heightInch = 2.52)
=> Document(doc =>
{
var page = doc.GetPage(pageIndex);
design(doc.GetPage(pageIndex));
});
public Pdf<TInputStream, TOutputStream> Page(Action<PdfPage> design)
{
for (int i = 1; i <= _doc.GetNumberOfPages(); i++)
Page(i, design);
return this;
}
public Pdf<TInputStream, TOutputStream> Design(int pageIndex, Action<PdfCanvas> design)
=> Document(doc =>
{
var page = doc.GetPage(pageIndex);
var canvas = new PdfCanvas(page);
design(canvas);
});
#endregion
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52)
where TSignature : IDocReceiverElement where TSignature : IDocReceiverElement
{ {
foreach (var signature in signatures) using (var gdpdf = new GdPicturePDF())
Page(signature.Page, page =>
{ {
var canvas = new PdfCanvas(page); GdPictureStatus loadStatus = gdpdf.LoadFromStream(new MemoryStream(_pdfBytes));
canvas.SaveState(); if (loadStatus != GdPictureStatus.OK)
canvas.ConcatMatrix(1, 0, 0, -1, 0, page.GetPageSize().GetHeight()); throw new InvalidOperationException("GdPicturePDF.LoadFromStream failed: " + loadStatus);
double inchFactor = 72; // Top-left origin + inch units = same coordinate system as GdPicture Form app annotations.
double magin = .2; // DB X and Y (inches from top-left) can be used as-is, no conversion needed.
double x = (signature.X - .7 - magin) * inchFactor; gdpdf.SetOrigin(PdfOrigin.PdfOriginTopLeft);
double y = (signature.Y - .5 - magin) * inchFactor; gdpdf.SetMeasurementUnit(PdfMeasurementUnit.PdfMeasurementUnitInch);
double width = widthPx * inchFactor;
double height = heightPx * inchFactor;
double bottomLineLength = 2.5; // Bottom accent line height: 2.5 PDF points converted to inches (1 inch = 72 pt)
const float bottomLineHeightInch = 2.5f / 72.0f;
// draw background foreach (var sig in signatures)
canvas.SetFillColor(new DeviceRgb(222, 220, 215)) {
.Rectangle(x, y, width, height) gdpdf.SelectPage(sig.Page);
.Fill();
// draw bottom line // sig.Width/Height come from DB (the actual stored dimensions).
canvas.SetFillColor(new DeviceRgb(204, 202, 198)) // Fall back to the method parameters only if the element has no stored size.
.Rectangle(x, y + height - bottomLineLength, width, bottomLineLength) float w = (float)widthInch; // sig.Width > 0 ? (float)sig.Width : (float)widthInch;
.Fill(); float h = (float)heightInch; // sig.Height > 0 ? (float)sig.Height : (float)heightInch;
canvas.RestoreState(); // annotation.Left/Top store the CENTER of the bounding box (GdPicture spec).
}); // DrawRectangle expects the TOP-LEFT corner → subtract half dimensions.
float x = (float)sig.X;
float y = (float)sig.Y;
// Background fill — DeviceRgb(222, 220, 215)
// DrawRectangle(Left, Top, Width, Height, Fill, Stroke)
gdpdf.SetFillColor(222, 220, 215);
gdpdf.DrawRectangle(x, y, w, h, true, false);
// Bottom accent line — DeviceRgb(204, 202, 198)
gdpdf.SetFillColor(204, 202, 198);
gdpdf.DrawRectangle(x, y + h - bottomLineHeightInch, w, bottomLineHeightInch, true, false);
}
var outStream = new MemoryStream();
GdPictureStatus saveStatus = gdpdf.SaveToStream(outStream);
gdpdf.CloseDocument();
if (saveStatus != GdPictureStatus.OK)
throw new InvalidOperationException("GdPicturePDF.SaveToStream failed: " + saveStatus);
_pdfBytes = outStream.ToArray();
}
return this; return this;
} }
#region Finalizer #region Dispose
private bool _disposed = false; private bool _disposed = false;
~Pdf() ~Pdf()
{ {
// If Dispose is not called, clean up unmanaged resources
Dispose(false); Dispose(false);
} }
@@ -154,23 +169,12 @@ namespace EnvelopeGenerator.PdfEditor
private void Dispose(bool disposing) private void Dispose(bool disposing)
{ {
if (_disposed) if (_disposed) return;
return;
if (disposing) if (disposing)
{ {
// Managed resources
_doc?.Close();
if (_disposeInputStream) _inputStream?.Dispose(); if (_disposeInputStream) _inputStream?.Dispose();
if(_disposeOutputStream) _outputStream?.Dispose(); if (_disposeOutputStream) _outputStream?.Dispose();
}
else
{
// When called by the finalizer, clean up only unmanaged resources
// Unmanaged resources such as PdfDocument, PdfReader, and PdfWriter are already IDisposable; we close them here
try { _doc?.Close(); } catch { }
try { if(_disposeInputStream) _inputStream?.Dispose(); } catch { }
try { if (_disposeOutputStream) _outputStream?.Dispose(); } catch { }
} }
_disposed = true; _disposed = true;
@@ -178,16 +182,14 @@ namespace EnvelopeGenerator.PdfEditor
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_disposed) if (_disposed) return;
return;
_doc?.Close();
if (_disposeInputStream) if (_disposeInputStream)
{ {
if (_inputStream is IAsyncDisposable asyncInput) if (_inputStream is IAsyncDisposable asyncInput)
await asyncInput.DisposeAsync(); await asyncInput.DisposeAsync();
else else
_inputStream.Dispose(); _inputStream?.Dispose();
} }
if (_disposeOutputStream) if (_disposeOutputStream)

View File

@@ -1,41 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net4.6.2</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-EnvelopeGenerator.Service-816256a4-c636-4313-92c0-b62b04040c2b</UserSecretsId>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0" />
<PackageReference Include="NLog" Version="5.3.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.9" />
</ItemGroup>
<!-- DigitalData & Quartz: local packages, must match CommonServices versions exactly -->
<ItemGroup>
<Reference Include="DigitalData.Modules.Database">
<HintPath>..\packages\DigitalData.Modules.Database.2.3.5.4\lib\net462\DigitalData.Modules.Database.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Modules.Encryption">
<HintPath>..\packages\DigitalData.Modules.Encryption.1.3.1\lib\net462\DigitalData.Modules.Encryption.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Modules.Logging">
<HintPath>..\packages\DigitalData.Modules.Logging.2.6.5\lib\net462\DigitalData.Modules.Logging.dll</HintPath>
</Reference>
<!-- Quartz 3.8.0 <20> must match CommonServices (same AppDomain, one version) -->
<Reference Include="Quartz">
<HintPath>..\packages\Quartz.3.8.0\lib\net462\Quartz.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj" />
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,28 +0,0 @@
using DigitalData.Modules.Logging;
namespace EnvelopeGenerator.Service;
/// <summary>
/// Builds a <see cref="LogConfig"/> from <see cref="ServiceConfig"/>.
/// Required because CommonServices jobs have a hard dependency on <see cref="LogConfig"/>
/// in their Quartz <c>JobDataMap</c>.
/// Log path mirrors the legacy behavior: <c>&lt;BaseDirectory&gt;\Log</c>.
/// </summary>
internal static class LogConfigFactory
{
public static LogConfig Create(ServiceConfig config)
{
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
var logConfig = new LogConfig(
LogConfig.PathType.CustomPath,
logPath,
null,
"Digital Data",
"EnvelopeGenerator.Service");
logConfig.Debug = config.Debug;
return logConfig;
}
}

View File

@@ -1,315 +0,0 @@
# Migration Plan: EnvelopeGenerator.Service_legacy ? EnvelopeGenerator.Service
**Revision v3** — Full deep analysis complete
## Overview
Migrate the legacy VB.NET Windows Service (`EnvelopeGenerator.Service_legacy`) to a modern
C# Worker Service (`EnvelopeGenerator.Service`) targeting **.NET Framework 4.6.2**, using
`Microsoft.Extensions.Hosting`, `IConfiguration`, `ILogger<T>` and **NLog**.
---
## Deep Analysis Findings (vs. v1/v2)
### Finding 1 — Connection String: Plain Text, No Encryption Required
`FinalizeDocumentJob` and `APIEnvelopeJob` call `MSSQLServer.DecryptConnectionString()` on the
`JobDataMap[Value.DATABASE]` value. However, `MSSQLServer.DecryptConnectionString()` is a pass-through
for plain-text strings — no wrapping/encryption step is needed.
**Resolution**: The connection string is read from `IConfiguration` via the standard
`ConnectionStrings:Default` key (same key as `appsettings.Database.json` in `EnvelopeGenerator.Web`).
It is passed **as-is** to both the Worker-level `MSSQLServer` and the `JobDataMap`. No
`EncryptConnectionString()` call is made anywhere in the new service.
### Finding 2 — Dual TempFiles Architecture
There are **two independent** `TempFiles` classes:
- `EnvelopeGenerator.CommonServices.TempFiles` — used **inside each job** via `New TempFiles(LogConfig)`.
This is called on every job execution and manages the job's own temp lifecycle. It expects `LogConfig`.
**We do not touch this.**
- `EnvelopeGenerator.Service_legacy.TempFiles` — used at the **service/host level** (startup cleanup,
shutdown cleanup). This is the one we rewrite in C# in the Service project using `ILogger<TempFiles>`.
### Finding 3 — LogConfig is a Hard Dependency of All Jobs
`FinalizeDocumentJob.Execute()` and `APIEnvelopeJob.Execute()` both do:
```vb
LogConfig = pContext.MergedJobDataMap.Item(Value.LOGCONFIG)
Logger = LogConfig.GetLogger()
myTempFiles = New TempFiles(LogConfig) ' ? CommonServices.TempFiles
```
`LogConfig` is **not optional**. The schedulers must construct a valid `LogConfig` and place it in the
`JobDataMap`. It must carry the correct log path, debug flag, and application identity.
**Resolution**: A `LogConfigFactory` static helper class will be created in the Service project to
build a `LogConfig` from `ServiceConfig` values:
- `LogPath` = `Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log")` (mirrors legacy `My.Application.Info.DirectoryPath + "\Log"`)
- `Debug` = `ServiceConfig.Debug`
- Application name = `"EnvelopeGenerator.Service"`
### Finding 4 — ProjectInstaller is Obsolete
`ProjectInstaller.vb` was the old `installutil.exe` mechanism for Windows Service registration.
With `AddWindowsService()` + `sc create` / PowerShell `New-Service`, this is **completely replaced**.
No equivalent is needed in the new project.
### Finding 5 — GDPicture License SQL
The legacy service queries: `SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' and ACTIVE = 1`
This must be replicated exactly in the Worker startup sequence. If the result is null/empty, startup fails
with a descriptive exception (matching legacy behavior).
### Finding 6 — Quartz Version Mismatch
`CommonServices` uses `Quartz 3.8.0`. `Service_legacy` uses `Quartz 3.15.0`.
The new `EnvelopeGenerator.Service` must use **Quartz 3.8.0** to match `CommonServices` (same AppDomain,
same version must be loaded). The legacy version bump in `Service_legacy` was inconsistent.
---
## 1. Project File (`EnvelopeGenerator.Service.csproj`)
- Add **NuGet packages**:
- `NLog` (5.x, matching CommonServices' `NLog.5.0.5`)
- `NLog.Extensions.Logging` (latest net462-compatible)
- `Quartz` (**3.8.0** — must match CommonServices)
- `Microsoft.Extensions.Hosting.WindowsServices` (for `AddWindowsService()`)
- Add **project reference** to `EnvelopeGenerator.CommonServices` (vbproj)
- Add **project reference** to `EnvelopeGenerator.Domain` (csproj)
---
## 2. Configuration (`ServiceConfig.cs`)
```csharp
public class ServiceConfig
{
public string ConnectionString { get; set; } = string.Empty;
public bool Debug { get; set; } = false;
public int IntervalInMin { get; set; } = 1;
public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams();
}
```
Connection string is read via the standard `ConnectionStrings:Default` key — consistent with
`appsettings.Database.json` used by `EnvelopeGenerator.Web`:
`appsettings.json`:
```json
{
"ConnectionStrings": {
"Default": ""
},
"ServiceConfig": {
"Debug": false,
"IntervalInMin": 1,
"PDFBurnerParams": {
"IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ],
"TopMargin": 0.1,
"YOffset": -0.3,
"FontName": "Arial",
"FontSize": 8,
"FontStyle": 2,
"TruncationSuffix": "..",
"TextMaxWidths": {
"position": 1.41,
"city": 1.41,
"date": 2.0
}
}
}
}
```
> `FontStyle` integer: Regular=0, Bold=1, **Italic=2**, Underline=4, Strikeout=8.
> All values mirror `PDFBurnerParams.vb` defaults exactly.
---
## 3. NLog Setup
**`nlog.config`**: File target ? `${basedir}/Log/service-${shortdate}.log`, async wrapper,
layout with timestamp + level + logger + message + exception.
**`Program.cs`**: Wire via `NLog.Extensions.Logging`:
```csharp
builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");
```
---
## 4. LogConfigFactory (`LogConfigFactory.cs`)
New static helper — bridges `ServiceConfig` ? `LogConfig` (required by CommonServices jobs):
```csharp
internal static class LogConfigFactory
{
public static LogConfig Create(ServiceConfig config)
{
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
var logConfig = new LogConfig(
LogConfig.PathType.CustomPath, logPath, null,
"Digital Data", "EnvelopeGenerator.Service");
logConfig.Debug = config.Debug;
return logConfig;
}
}
```
This `LogConfig` instance is what gets placed into every `JobDataMap[Value.LOGCONFIG]`.
---
## 5. Quartz LogProvider (`QuartzLogProvider.cs`)
C# rewrite of `LogProvider.vb` — bridges Quartz `ILogProvider` ? `ILogger<T>`:
```csharp
internal class QuartzLogProvider : ILogProvider
{
private readonly ILogger _logger;
public QuartzLogProvider(ILogger logger) => _logger = logger;
public Logger GetLogger(string name) => (level, func, ex, parameters) => { ... };
// OpenNestedContext / OpenMappedContext ? NotImplementedException (same as legacy)
}
```
---
## 6. TempFiles (`TempFiles.cs`) — Service-level only
Service-level rewrite of `Service_legacy/TempFiles.vb` using `ILogger<TempFiles>`:
- `Create()` ? create `%TEMP%\EnvelopeGenerator`, or clean existing files
- `CleanUp()` ? delete the directory on service stop
> **Does NOT replace** `CommonServices.TempFiles` — that one continues to be used
> inside each job via `LogConfig`.
---
## 7. Schedulers
### `Schedulers/SchedulerFinishEnvelope.cs`
```csharp
public class SchedulerFinishEnvelope
{
// Constructor: ILogger<SchedulerFinishEnvelope>, IOptions<ServiceConfig>
// Start(licenseKey, logConfig):
// - Build JobDataMap:
// [GDPICTURE] = licenseKey
// [LOGCONFIG] = logConfig ? LogConfigFactory output
// [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
// [PDF_BURNER_PARAMS] = config.PDFBurnerParams
// - Schedule FinalizeDocumentJob with Quartz (interval from config)
// - Register QuartzLogProvider
// Stop(): Scheduler.Shutdown()
}
```
### `Schedulers/SchedulerEnvelopeTaskApi.cs`
```csharp
public class SchedulerEnvelopeTaskApi
{
// Constructor: ILogger<SchedulerEnvelopeTaskApi>, IOptions<ServiceConfig>
// Start(logConfig):
// - Build JobDataMap:
// [LOGCONFIG] = logConfig
// [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
// - Schedule APIEnvelopeJob with Quartz (interval from config)
// - Register QuartzLogProvider
// Stop(): Scheduler.Shutdown()
}
```
---
## 8. Worker (`Worker.cs`)
Full lifecycle replacing the placeholder:
```
ExecuteAsync:
1. Read ServiceConfig from IOptions<ServiceConfig>
2. Validate ConnectionString ? throw if empty
3. Build LogConfig via LogConfigFactory
4. Connect to DB (plain connection string — Worker-level only)
5. Query: SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME='GDPICTURE' AND ACTIVE=1
? throw if null/empty (matches legacy behavior)
6. TempFiles.Create()
7. await SchedulerFinishEnvelope.Start(licenseKey, logConfig)
8. await Task.Delay(2500, stoppingToken) ? preserved from legacy
9. await SchedulerEnvelopeTaskApi.Start(logConfig)
10. await Task.Delay(Timeout.Infinite, stoppingToken) ? wait for cancellation
StopAsync override:
11. await SchedulerFinishEnvelope.Stop()
12. await SchedulerEnvelopeTaskApi.Stop()
13. TempFiles.CleanUp()
```
---
## 9. Program.cs
```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");
builder.Services.Configure<ServiceConfig>(
builder.Configuration.GetSection("ServiceConfig"));
builder.Services.AddSingleton<TempFiles>();
builder.Services.AddSingleton<SchedulerFinishEnvelope>();
builder.Services.AddSingleton<SchedulerEnvelopeTaskApi>();
builder.Services.AddHostedService<Worker>();
builder.Services.AddWindowsService(o => o.ServiceName = "EnvelopeGenerator.Service");
var host = builder.Build();
host.Run();
```
---
## 10. Files to Create / Modify
| File | Action | Notes |
|------|--------|-------|
| `EnvelopeGenerator.Service.csproj` | **Modify** | Add packages, project refs |
| `Program.cs` | **Modify** | Full DI/NLog/WindowsService setup |
| `Worker.cs` | **Modify** | Full service lifecycle |
| `ServiceConfig.cs` | **Create** | |
| `LogConfigFactory.cs` | **Create** | Critical bridge: ServiceConfig ? LogConfig |
| `TempFiles.cs` | **Create** | Service-level only; CommonServices.TempFiles untouched |
| `QuartzLogProvider.cs` | **Create** | |
| `Schedulers/SchedulerFinishEnvelope.cs` | **Create** | |
| `Schedulers/SchedulerEnvelopeTaskApi.cs` | **Create** | |
| `appsettings.json` | **Create** | All PDFBurnerParams explicit |
| `nlog.config` | **Create** | |
**Not created** (obsolete in new architecture):
- ~~`ProjectInstaller`~~ ? replaced by `sc create` / `New-Service`
- ~~`Service.Designer.vb`~~ ? BackgroundService handles this
- ~~`Service.resx`~~ ? not applicable
---
## 11. Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| **Quartz 3.8.0** (not 3.15.0) | Must match CommonServices; same AppDomain, one version |
| **Connection string plain-text, no encryption** | `MSSQLServer.DecryptConnectionString()` is a pass-through for plain strings; `ConnectionStrings:Default` key used (same convention as Web project) |
| **Connection string in `ConnectionStrings:Default`** | Consistent with rest of the solution (`appsettings.Database.json`); `ServiceConfig` holds only service-specific settings |
| **LogConfigFactory** | Jobs have hard dependency on `LogConfig` in JobDataMap; this is the clean bridge without leaking LogConfig into the DI container |
| **2500ms delay preserved** | Legacy behavior; ensures Scheduler1 is fully initialized before Scheduler2 starts |
| **`AddWindowsService()`** | Native SCM integration; replaces ProjectInstaller/installutil entirely |
| **NLog log path = BaseDirectory/Log** | Mirrors `My.Application.Info.DirectoryPath + "\Log"` from legacy |
| **CommonServices.TempFiles untouched** | Jobs create their own TempFiles internally via LogConfig; service-level TempFiles is separate concern |
---
## Approval Required
> Please review this plan and confirm before implementation begins.

View File

@@ -1,41 +0,0 @@
using EnvelopeGenerator.Service;
using EnvelopeGenerator.Service.Schedulers;
using Microsoft.Extensions.Options;
using NLog.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
// NLog
builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");
// Configuration
builder.Services.Configure<ServiceConfig>(
builder.Configuration.GetSection("ServiceConfig"));
// Resolve connection string once for singleton registrations
var connectionString = builder.Configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("ConnectionStrings:Default is not configured.");
// Services
builder.Services.AddSingleton<TempFiles>();
builder.Services.AddSingleton<SchedulerFinishEnvelope>(sp => new SchedulerFinishEnvelope(
sp.GetRequiredService<ILogger<SchedulerFinishEnvelope>>(),
sp.GetRequiredService<IOptions<ServiceConfig>>(),
connectionString));
builder.Services.AddSingleton<SchedulerEnvelopeTaskApi>(sp => new SchedulerEnvelopeTaskApi(
sp.GetRequiredService<ILogger<SchedulerEnvelopeTaskApi>>(),
sp.GetRequiredService<IOptions<ServiceConfig>>(),
connectionString));
builder.Services.AddHostedService<Worker>();
// Windows Service (SCM) support — replaces legacy ProjectInstaller/installutil
builder.Services.AddWindowsService(options =>
options.ServiceName = "EnvelopeGenerator.Service");
var host = builder.Build();
host.Run();

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net4.6.2\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -1,12 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"EnvelopeGenerator.Service": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,60 +0,0 @@
using Quartz.Logging;
using QuartzLogLevel = Quartz.Logging.LogLevel;
namespace EnvelopeGenerator.Service;
/// <summary>
/// Bridges Quartz's <see cref="ILogProvider"/> to <see cref="ILogger"/>.
/// Mirrors the legacy <c>LogProvider.vb</c> behavior.
/// <c>OpenNestedContext</c> and <c>OpenMappedContext</c> are not implemented (same as legacy).
/// </summary>
internal sealed class QuartzLogProvider : ILogProvider
{
private readonly ILogger _logger;
public QuartzLogProvider(ILogger logger)
{
_logger = logger;
}
public Logger GetLogger(string name)
{
return (level, func, exception, parameters) =>
{
if (exception != null)
{
_logger.LogError(exception, exception.Message);
}
else if (func != null)
{
switch (level)
{
case QuartzLogLevel.Trace:
case QuartzLogLevel.Debug:
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
_logger.LogDebug(func(), parameters);
break;
case QuartzLogLevel.Info:
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information))
_logger.LogInformation(func(), parameters);
break;
case QuartzLogLevel.Warn:
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning))
_logger.LogWarning(func(), parameters);
break;
case QuartzLogLevel.Error:
case QuartzLogLevel.Fatal:
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Error))
_logger.LogError(func(), parameters);
break;
}
}
return true;
};
}
public IDisposable OpenNestedContext(string message) => throw new NotImplementedException();
public IDisposable OpenMappedContext(string key, object value, bool destructure = false) => throw new NotImplementedException();
}

View File

@@ -1,94 +0,0 @@
using System.Collections.Specialized;
using DigitalData.Modules.Logging;
using EnvelopeGenerator.CommonServices.Jobs;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.Extensions.Options;
using Quartz;
using Quartz.Logging;
namespace EnvelopeGenerator.Service.Schedulers;
/// <summary>
/// Schedules <see cref="APIEnvelopeJob"/> via Quartz.
/// C# rewrite of the legacy <c>Scheduler_Envelopetask_API.vb</c>.
/// </summary>
public sealed class SchedulerEnvelopeTaskApi
{
private readonly ILogger<SchedulerEnvelopeTaskApi> _logger;
private readonly ServiceConfig _config;
private readonly string _connectionString;
private IScheduler? _scheduler;
private const string JobName = "SendInvitationMailsAPI";
public SchedulerEnvelopeTaskApi(
ILogger<SchedulerEnvelopeTaskApi> logger,
IOptions<ServiceConfig> config,
string connectionString)
{
_logger = logger;
_config = config.Value;
_connectionString = connectionString;
}
public async Task Start(LogConfig logConfig)
{
try
{
_logger.LogDebug("Starting SchedulerEnvelopeTaskApi...");
LogProvider.SetCurrentLogProvider(new QuartzLogProvider(_logger));
_scheduler = await SchedulerBuilder
.Create(new NameValueCollection())
.UseDefaultThreadPool(x => x.MaxConcurrency = 5)
.BuildScheduler();
var jobKey = new JobKey(JobName);
var jobData = new JobDataMap
{
{ Value.LOGCONFIG, logConfig },
{ Value.DATABASE, _connectionString }
};
_logger.LogDebug("Initialized Job [{JobName}]", JobName);
var job = JobBuilder.Create<APIEnvelopeJob>()
.UsingJobData(jobData)
.WithIdentity(jobKey)
.Build();
var trigger = TriggerBuilder.Create()
.ForJob(jobKey)
.WithIdentity($"{JobName}-trigger")
.WithSimpleSchedule(s => s
.RepeatForever()
.WithIntervalInMinutes(_config.IntervalInMin))
.StartNow()
.Build();
_logger.LogDebug("{JobName}-trigger initialized", JobName);
await _scheduler.ScheduleJob(job, trigger);
_logger.LogDebug("{JobName} scheduled", JobName);
await _scheduler.Start();
_logger.LogInformation("{JobName} started", JobName);
}
catch (Exception ex)
{
_logger.LogError(ex, "SchedulerEnvelopeTaskApi failed to start");
throw;
}
}
public async Task Stop()
{
if (_scheduler is null) return;
_logger.LogInformation("Stopping SchedulerEnvelopeTaskApi...");
await _scheduler.Shutdown();
_logger.LogInformation("SchedulerEnvelopeTaskApi stopped!");
}
}

View File

@@ -1,97 +0,0 @@
using System.Collections.Specialized;
using DigitalData.Modules.Logging;
using EnvelopeGenerator.CommonServices.Jobs;
using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.Extensions.Options;
using Quartz;
using Quartz.Logging;
namespace EnvelopeGenerator.Service.Schedulers;
/// <summary>
/// Schedules <see cref="FinalizeDocumentJob"/> via Quartz.
/// C# rewrite of the legacy <c>Scheduler_FinishEnvelope.vb</c>.
/// </summary>
public sealed class SchedulerFinishEnvelope
{
private readonly ILogger<SchedulerFinishEnvelope> _logger;
private readonly ServiceConfig _config;
private readonly string _connectionString;
private IScheduler? _scheduler;
private const string JobName = "CertificateDocumentJob";
public SchedulerFinishEnvelope(
ILogger<SchedulerFinishEnvelope> logger,
IOptions<ServiceConfig> config,
string connectionString)
{
_logger = logger;
_config = config.Value;
_connectionString = connectionString;
}
public async Task Start(string licenseKey, LogConfig logConfig)
{
try
{
_logger.LogDebug("Starting SchedulerFinishEnvelope...");
LogProvider.SetCurrentLogProvider(new QuartzLogProvider(_logger));
_scheduler = await SchedulerBuilder
.Create(new NameValueCollection())
.UseDefaultThreadPool(x => x.MaxConcurrency = 5)
.BuildScheduler();
var jobKey = new JobKey(JobName);
var jobData = new JobDataMap
{
{ Value.GDPICTURE, licenseKey },
{ Value.LOGCONFIG, logConfig },
{ Value.DATABASE, _connectionString },
{ Value.PDF_BURNER_PARAMS, _config.PDFBurnerParams }
};
_logger.LogDebug("Initialized Job [{JobName}]", JobName);
var job = JobBuilder.Create<FinalizeDocumentJob>()
.UsingJobData(jobData)
.WithIdentity(jobKey)
.Build();
var trigger = TriggerBuilder.Create()
.ForJob(jobKey)
.WithIdentity($"{JobName}-trigger")
.WithSimpleSchedule(s => s
.RepeatForever()
.WithIntervalInMinutes(_config.IntervalInMin))
.StartNow()
.Build();
_logger.LogDebug("Initialized Trigger");
await _scheduler.ScheduleJob(job, trigger);
_logger.LogDebug("Job scheduled.");
await _scheduler.Start();
_logger.LogDebug("Scheduler started!");
}
catch (Exception ex)
{
_logger.LogError(ex, "SchedulerFinishEnvelope failed to start");
throw;
}
}
public async Task Stop()
{
if (_scheduler is null) return;
_logger.LogInformation("Stopping SchedulerFinishEnvelope...");
await _scheduler.Shutdown();
_logger.LogInformation("SchedulerFinishEnvelope stopped!");
}
}

View File

@@ -1,14 +0,0 @@
using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument;
namespace EnvelopeGenerator.Service;
/// <summary>
/// Service-level configuration bound from the "ServiceConfig" section of appsettings.json.
/// Connection string is read separately via the standard "ConnectionStrings:Default" key.
/// </summary>
public class ServiceConfig
{
public bool Debug { get; set; } = false;
public int IntervalInMin { get; set; } = 1;
public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams();
}

View File

@@ -1,75 +0,0 @@
namespace EnvelopeGenerator.Service;
/// <summary>
/// Service-level temp file management. Mirrors <c>TempFiles.vb</c> from the legacy project.
/// Manages <c>%TEMP%\EnvelopeGenerator</c> at host startup and shutdown.
///
/// NOTE: This is independent from <c>EnvelopeGenerator.CommonServices.TempFiles</c>, which is
/// instantiated inside each Quartz job via <see cref="DigitalData.Modules.Logging.LogConfig"/>.
/// Do not confuse the two.
/// </summary>
public sealed class TempFiles
{
private readonly ILogger<TempFiles> _logger;
public string TempPath { get; }
public TempFiles(ILogger<TempFiles> logger)
{
_logger = logger;
TempPath = Path.Combine(Path.GetTempPath(), "EnvelopeGenerator");
}
public bool Create()
{
try
{
if (!Directory.Exists(TempPath))
{
Directory.CreateDirectory(TempPath);
}
else
{
CleanUpFiles();
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create temp directory: {TempPath}", TempPath);
return false;
}
}
private void CleanUpFiles()
{
try
{
foreach (var file in Directory.GetFiles(TempPath))
{
_logger.LogDebug("Deleting temp file: {File}", file);
File.Delete(file);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clean up temp files in: {TempPath}", TempPath);
}
}
public bool CleanUp()
{
try
{
_logger.LogDebug("Deleting temp directory: {TempPath}", TempPath);
Directory.Delete(TempPath, recursive: true);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete temp directory: {TempPath}", TempPath);
return false;
}
}
}

View File

@@ -1,111 +0,0 @@
using DigitalData.Modules.Database;
using EnvelopeGenerator.Service.Schedulers;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Service;
public sealed class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly ServiceConfig _config;
private readonly string _connectionString;
private readonly TempFiles _tempFiles;
private readonly SchedulerFinishEnvelope _schedulerFinishEnvelope;
private readonly SchedulerEnvelopeTaskApi _schedulerEnvelopeTaskApi;
public Worker(
ILogger<Worker> logger,
IOptions<ServiceConfig> config,
IConfiguration configuration,
TempFiles tempFiles,
SchedulerFinishEnvelope schedulerFinishEnvelope,
SchedulerEnvelopeTaskApi schedulerEnvelopeTaskApi)
{
_logger = logger;
_config = config.Value;
_connectionString = configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("ConnectionStrings:Default is not configured.");
_tempFiles = tempFiles;
_schedulerFinishEnvelope = schedulerFinishEnvelope;
_schedulerEnvelopeTaskApi = schedulerEnvelopeTaskApi;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("EnvelopeGenerator.Service starting...");
// 1. Validate connection string
if (string.IsNullOrWhiteSpace(_connectionString))
throw new InvalidOperationException("Connection string is empty!");
// 2. Build LogConfig (required by CommonServices jobs via JobDataMap)
var logConfig = LogConfigFactory.Create(_config);
_logger.LogInformation("DEBUG = {Debug}", _config.Debug);
// 3. Connect to DB and load GDPicture license
_logger.LogDebug("Initializing Database...");
var database = new MSSQLServer(logConfig, _connectionString);
if (!database.DBInitialized)
throw new InvalidOperationException("Database connection could not be established!");
_logger.LogDebug("Database initialized.");
var licenseKey = database.GetScalarValue(
"SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' AND ACTIVE = 1") as string;
if (string.IsNullOrWhiteSpace(licenseKey))
throw new InvalidOperationException("GDPicture license could not be loaded!");
_logger.LogDebug("GDPicture license loaded.");
// 4. Create service-level temp directory
_tempFiles.Create();
// 5. Start FinalizeDocument scheduler
_logger.LogDebug("Initializing schedulers...");
await _schedulerFinishEnvelope.Start(licenseKey!, logConfig);
// 6. Delay between schedulers (legacy behavior preserved)
await Task.Delay(2500, stoppingToken);
// 7. Start API envelope scheduler
await _schedulerEnvelopeTaskApi.Start(logConfig);
_logger.LogInformation("EnvelopeGenerator.Service started!");
// 8. Wait for cancellation (service runs until stopped)
await Task.Delay(Timeout.Infinite, stoppingToken);
}
catch (OperationCanceledException)
{
// Normal shutdown <20> not an error
}
catch (Exception ex)
{
_logger.LogError(ex, "EnvelopeGenerator.Service encountered a fatal error during startup.");
throw;
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("EnvelopeGenerator.Service stopping...");
try
{
await _schedulerFinishEnvelope.Stop();
await _schedulerEnvelopeTaskApi.Stop();
_tempFiles.CleanUp();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during service shutdown.");
}
await base.StopAsync(cancellationToken);
_logger.LogInformation("EnvelopeGenerator.Service stopped!");
}
}

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -1,24 +0,0 @@
{
"ConnectionStrings": {
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
},
"ServiceConfig": {
"Debug": false,
"IntervalInMin": 1,
"PDFBurnerParams": {
"IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ],
"TopMargin": 0.1,
"YOffset": -0.3,
"FontName": "Arial",
"FontSize": 8,
"FontStyle": 2,
"TruncationSuffix": "..",
"TextMaxWidths": {
"position": 1.41,
"city": 1.41,
"date": 2.0
}
}
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true">
<targets async="true">
<target xsi:type="File"
name="file"
fileName="${basedir}/Log/service-${shortdate}.log"
layout="${longdate} | ${level:uppercase=true} | ${logger} | ${message}${onexception:${newline}${exception:format=tostring}}" />
<target xsi:type="Console"
name="console"
layout="${longdate} | ${level:uppercase=true} | ${logger} | ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="file,console" />
</rules>
</nlog>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<PackageId>EnvelopeGenerator.Web</PackageId> <PackageId>EnvelopeGenerator.Web</PackageId>
@@ -2169,6 +2169,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" /> <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.11" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup> </ItemGroup>

View File

@@ -23,20 +23,19 @@
"Annotations": [ "Annotations": [
{ {
"Name": "Signature", "Name": "Signature",
"MarginTop": 0, "MarginTop": 0
"MarginLeft": 0.20
}, },
{ {
"Name": "PositionLabel", "Name": "PositionLabel",
"VerBoundAnnotName": "Signature", "VerBoundAnnotName": "Signature",
"WidthRatio": 1.2, "WidthRatio": 1.2,
"HeightRatio": 0.5, "HeightRatio": 0.5,
"MarginTopRatio": 0.30 "MarginTopRatio": 0.22
}, },
{ {
"Name": "Position", "Name": "Position",
"VerBoundAnnotName": "PositionLabel", "VerBoundAnnotName": "PositionLabel",
"WidthRatio": 1.4, "WidthRatio": 1.2,
"HeightRatio": 0.5, "HeightRatio": 0.5,
"MarginTopRatio": -0.05 "MarginTopRatio": -0.05
}, },
@@ -50,7 +49,7 @@
{ {
"Name": "City", "Name": "City",
"VerBoundAnnotName": "CityLabel", "VerBoundAnnotName": "CityLabel",
"WidthRatio": 1.4, "WidthRatio": 1.2,
"HeightRatio": 0.5, "HeightRatio": 0.5,
"MarginTopRatio": -0.05 "MarginTopRatio": -0.05
}, },

View File

@@ -11,16 +11,13 @@ async function createAnnotations(document, envelopeId, receiverId) {
//#region signatures //#region signatures
const id = generateId(envelopeId, receiverId, element.id, 'signature'); const id = generateId(envelopeId, receiverId, element.id, 'signature');
const boundingBox = new PSPDFKit.Geometry.Rect(annotParams.signature);
const annotation = new PSPDFKit.Annotations.WidgetAnnotation({ const annotation = new PSPDFKit.Annotations.WidgetAnnotation({
id: id, id: id,
pageIndex: page, pageIndex: page,
formFieldName: id, formFieldName: id,
backgroundColor: null, backgroundColor: PSPDFKit.Color.LIGHT_YELLOW,
blendMode: 'normal', blendMode: 'normal',
boundingBox: boundingBox, boundingBox: new PSPDFKit.Geometry.Rect(annotParams.signature),
}) })
const formField = new PSPDFKit.FormFields.SignatureFormField({ const formField = new PSPDFKit.FormFields.SignatureFormField({
@@ -240,7 +237,7 @@ async function createAnnotationFrameBlob(receiverName, receiverSignature, timest
Comp.SignatureProgress.SignedCount += 1; Comp.SignatureProgress.SignedCount += 1;
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
const scale = 4 const scale = 4
const fontSize = 8 const fontSize = 10
canvas.width = width * scale canvas.width = width * scale
canvas.height = height * scale canvas.height = height * scale
@@ -273,17 +270,21 @@ async function createAnnotationFrameBlob(receiverName, receiverSignature, timest
ctx.moveTo(30 * scale, 50 * scale) ctx.moveTo(30 * scale, 50 * scale)
ctx.lineTo(signatureLength, 50 * scale) ctx.lineTo(signatureLength, 50 * scale)
ctx.strokeStyle = 'darkgray' ctx.strokeStyle = 'darkblue'
ctx.stroke() ctx.stroke()
ctx.fillStyle = 'black' ctx.fillStyle = 'black'
ctx.font = `${fontSize * scale}px sans-serif` ctx.font = `${fontSize * scale}px sans-serif`
ctx.fillText((READ_AND_CONFIRM ? localized.confirmedBy : localized.signedBy) ?? 'Signed by', 15 * scale, 10 * scale) ctx.fillText((READ_AND_CONFIRM ? localized.confirmedBy : localized.signedBy) ?? 'Signed by', 15 * scale, 10 * scale)
ctx.fillText(receiverName, 15 * scale, 58.5 * scale) ctx.fillText(receiverName, 15 * scale, 60 * scale)
ctx.fillText(signatureString, 15 * scale, 69 * scale) ctx.fillText(signatureString, 15 * scale, 70 * scale)
const blob = await new Promise((resolve) => canvas.toBlob(resolve)) return new Promise((resolve) => {
return URL.createObjectURL(blob) canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob)
resolve(url)
})
})
} }
//required //required

View File

@@ -37,15 +37,10 @@ class App {
}); });
// Load annotations into PSPDFKit // Load annotations into PSPDFKit
this.signatureWidgets = [] // { pageIndex, boundingBox, id }
if (!READ_AND_CONFIRM) if (!READ_AND_CONFIRM)
try { try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId); let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures); await this.pdfKit.create(signatures);
// Cache signature widget bounding boxes for stable frame placement
this.signatureWidgets = signatures
.filter(s => s instanceof PSPDFKit.Annotations.WidgetAnnotation && s.formFieldName?.includes('signature'))
.map(s => ({ id: s.id, pageIndex: s.pageIndex, boundingBox: s.boundingBox.toJS() }))
} catch (e) { } catch (e) {
console.error("Error loading annotations:", e); console.error("Error loading annotations:", e);
} }
@@ -71,27 +66,8 @@ class App {
if (isFormField === false && isSignature === true) { if (isFormField === false && isSignature === true) {
// Find the nearest signature widget on the same page to use its stable bounding box const left = annotation.boundingBox.left - 20
const inkBBox = annotation.boundingBox const top = annotation.boundingBox.top - 20
const samePageWidgets = this.signatureWidgets.filter(w => w.pageIndex === annotation.pageIndex)
const nearestWidget = samePageWidgets.reduce((nearest, w) => {
const dx = w.boundingBox.left - inkBBox.left
const dy = w.boundingBox.top - inkBBox.top
const dist = Math.sqrt(dx * dx + dy * dy)
return dist < nearest.dist ? { dist, widget: w } : nearest
}, { dist: Infinity, widget: null }).widget
// 1. Remove widget background after signing
if (nearestWidget) {
const pageAnnots = await this.pdfKit.getAnnotations(annotation.pageIndex)
const widgetAnnot = pageAnnots.find(a => a.id === nearestWidget.id)
if (widgetAnnot)
await this.pdfKit.update(widgetAnnot.set('backgroundColor', PSPDFKit.Color.TRANSPARENT))
}
const bbox = nearestWidget?.boundingBox ?? inkBBox
const left = bbox.left - 20
const top = bbox.top - 12
const width = 150 const width = 150
const height = 75 const height = 75
const timestamp = new Date() const timestamp = new Date()
@@ -354,7 +330,7 @@ class App {
} }
async handleReset(event) { async handleReset(event) {
const result = await Swal.fire({ const result = Swal.fire({
title: localized.resetConfirmTitle, title: localized.resetConfirmTitle,
text: localized.resetConfirmText, text: localized.resetConfirmText,
icon: 'question', icon: 'question',
@@ -362,24 +338,7 @@ class App {
}) })
if (result.isConfirmed) { if (result.isConfirmed) {
await deleteAnnotations(this.pdfKit) const result = await deleteAnnotations(this.pdfKit)
// Reset editable text form field values (Position, City)
const allFormFields = await this.pdfKit.getFormFields()
const editableFields = allFormFields.filter(f =>
f instanceof PSPDFKit.FormFields.TextFormField && !f.readOnly
)
for (const field of editableFields) {
await this.pdfKit.setFormFieldValues({ [field.name]: '' })
}
// Restore widget backgrounds that were made transparent after signing
for (const w of this.signatureWidgets) {
const pageAnnots = await this.pdfKit.getAnnotations(w.pageIndex)
const widgetAnnot = pageAnnots.find(a => a.id === w.id)
if (widgetAnnot)
await this.pdfKit.update(widgetAnnot.set('backgroundColor', null))
}
} }
return result return result

View File

@@ -1,3 +1,3 @@
const formatLocalized=(n,...t)=>typeof n=="string"?n.replace(/\{(\d+)\}/g,(n,i)=>t[i]??""):"";class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),this.signatureWidgets=[],!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n);this.signatureWidgets=n.filter(n=>n instanceof PSPDFKit.Annotations.WidgetAnnotation&&n.formFieldName?.includes("signature")).map(n=>({id:n.id,pageIndex:n.pageIndex,boundingBox:n.boundingBox.toJS()}))}catch(n){console.error("Error loading annotations:",n)}READ_AND_CONFIRM||[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const n=t.boundingBox,e=this.signatureWidgets.filter(n=>n.pageIndex===t.pageIndex),i=e.reduce((t,i)=>{const r=i.boundingBox.left-n.left,u=i.boundingBox.top-n.top,f=Math.sqrt(r*r+u*u);return f<t.dist?{dist:f,widget:i}:t},{dist:Infinity,widget:null}).widget;if(i){const r=await this.pdfKit.getAnnotations(t.pageIndex),n=r.find(n=>n.id===i.id);n&&await this.pdfKit.update(n.set("backgroundColor",PSPDFKit.Color.TRANSPARENT))}const r=i?.boundingBox??n,o=r.left-20,s=r.top-12,u=150,f=75,h=new Date,c=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,h,u,f),l=await fetch(c),a=await l.blob(),v=await this.pdfKit.createAttachment(a),y=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:o,top:s,width:u,height:f}),t.pageIndex,v,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(y)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:localized.success,text:localized.documentReset,icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(` const formatLocalized=(n,...t)=>typeof n=="string"?n.replace(/\{(\d+)\}/g,(n,i)=>t[i]??""):"";class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}READ_AND_CONFIRM||[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:localized.success,text:localized.documentReset,icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
Request failed: ${t} Request failed: ${t}
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify(localized.copyLinkSuccess,{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify(localized.copyLinkFailure??localized.unexpectedErrorTitle,{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?formatLocalized(localized.viewRemainingPages,n.join(", ")):localized.viewAllPages;return await Swal.fire({title:localized.warning,text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:localized.warning,text:localized.locationFieldsRequired,icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:localized.warning,text:formatLocalized(localized.cityFormatInvalid,t.value),icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:localized.warning,text:localized.missingSignatures,icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${READ_AND_CONFIRM?localized.confirmAgree:localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:localized.warning,text:localized.envelopeUnavailable,icon:"warning"}),!1;if(t.status===423)Swal.fire({title:localized.info??localized.warning,text:localized.envelopeRejectedRedirect,icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=await Swal.fire({title:localized.resetConfirmTitle,text:localized.resetConfirmText,icon:"question",showCancelButton:!0});if(n.isConfirmed){await deleteAnnotations(this.pdfKit);const n=await this.pdfKit.getFormFields(),t=n.filter(n=>n instanceof PSPDFKit.FormFields.TextFormField&&!n.readOnly);for(const n of t)await this.pdfKit.setFormFieldValues({[n.name]:""});for(const n of this.signatureWidgets){const i=await this.pdfKit.getAnnotations(n.pageIndex),t=i.find(t=>t.id===n.id);t&&await this.pdfKit.update(t.set("backgroundColor",null))}}return n}fakeElementId=0;} `)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify(localized.copyLinkSuccess,{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify(localized.copyLinkFailure??localized.unexpectedErrorTitle,{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?formatLocalized(localized.viewRemainingPages,n.join(", ")):localized.viewAllPages;return await Swal.fire({title:localized.warning,text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:localized.warning,text:localized.locationFieldsRequired,icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:localized.warning,text:formatLocalized(localized.cityFormatInvalid,t.value),icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:localized.warning,text:localized.missingSignatures,icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${READ_AND_CONFIRM?localized.confirmAgree:localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:localized.warning,text:localized.envelopeUnavailable,icon:"warning"}),!1;if(t.status===423)Swal.fire({title:localized.info??localized.warning,text:localized.envelopeRejectedRedirect,icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:localized.resetConfirmTitle,text:localized.resetConfirmText,icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}

View File

@@ -9,6 +9,8 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.CommonSer
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Web", "EnvelopeGenerator.Web\EnvelopeGenerator.Web.csproj", "{5E0E17C0-FF5A-4246-BF87-1ADD85376A27}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Web", "EnvelopeGenerator.Web\EnvelopeGenerator.Web.csproj", "{5E0E17C0-FF5A-4246-BF87-1ADD85376A27}"
EndProject EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Service", "EnvelopeGenerator.Service\EnvelopeGenerator.Service.vbproj", "{83ED2617-B398-4859-8F59-B38F8807E83E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Domain", "EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj", "{4F32A98D-E6F0-4A09-BD97-1CF26107E837}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Domain", "EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj", "{4F32A98D-E6F0-4A09-BD97-1CF26107E837}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Infrastructure", "EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj", "{63E32615-0ECA-42DC-96E3-91037324B7C7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Infrastructure", "EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj", "{63E32615-0ECA-42DC-96E3-91037324B7C7}"
@@ -20,8 +22,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
fix-report-label-read-and-confirmed.md = fix-report-label-read-and-confirmed.md
fix-signature-field-formatting.md = fix-signature-field-formatting.md
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
EndProjectSection EndProjectSection
@@ -53,10 +53,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server",
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server.Client", "EnvelopeGenerator.Server\EnvelopeGenerator.Server.Client\EnvelopeGenerator.Server.Client.csproj", "{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server.Client", "EnvelopeGenerator.Server\EnvelopeGenerator.Server.Client\EnvelopeGenerator.Server.Client.csproj", "{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}"
EndProject EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Service_legacy", "EnvelopeGenerator.Service_legacy\EnvelopeGenerator.Service_legacy.vbproj", "{83ED2617-B398-4859-8F59-B38F8807E83E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Service", "EnvelopeGenerator.Service\EnvelopeGenerator.Service.csproj", "{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -75,6 +71,10 @@ Global
{5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Debug|Any CPU.Build.0 = Debug|Any CPU {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.ActiveCfg = Debug|Any CPU {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.Build.0 = Debug|Any CPU {5E0E17C0-FF5A-4246-BF87-1ADD85376A27}.Release|Any CPU.Build.0 = Debug|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Debug|Any CPU.Build.0 = Release|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.Build.0 = Debug|Any CPU
{4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Release|Any CPU.ActiveCfg = Debug|Any CPU {4F32A98D-E6F0-4A09-BD97-1CF26107E837}.Release|Any CPU.ActiveCfg = Debug|Any CPU
@@ -119,14 +119,6 @@ Global
{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Release|Any CPU.Build.0 = Release|Any CPU {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Release|Any CPU.Build.0 = Release|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83ED2617-B398-4859-8F59-B38F8807E83E}.Release|Any CPU.Build.0 = Release|Any CPU
{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -135,6 +127,7 @@ Global
{089D5634-FB6B-42D0-B912-7AA7457044E7} = {0CBC2432-A561-4440-89BC-671B66A24146} {089D5634-FB6B-42D0-B912-7AA7457044E7} = {0CBC2432-A561-4440-89BC-671B66A24146}
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB} {6EA0C51F-C2B1-4462-8198-3DE0B32B74F8} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{5E0E17C0-FF5A-4246-BF87-1ADD85376A27} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} {5E0E17C0-FF5A-4246-BF87-1ADD85376A27} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{83ED2617-B398-4859-8F59-B38F8807E83E} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{4F32A98D-E6F0-4A09-BD97-1CF26107E837} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB} {4F32A98D-E6F0-4A09-BD97-1CF26107E837} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{63E32615-0ECA-42DC-96E3-91037324B7C7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {63E32615-0ECA-42DC-96E3-91037324B7C7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB} {5A9984F8-51A2-4558-A415-EC5FEED7CF7D} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
@@ -150,8 +143,6 @@ Global
{BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} {BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0} {4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0}
{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0} {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0}
{83ED2617-B398-4859-8F59-B38F8807E83E} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7} SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}

View File

@@ -1,135 +0,0 @@
# Fix: Report Label "Read and confirmed" for Read and Sign Envelopes
## Status
✅ **COMPLETED**
## Git Branch
```
fix/report-label-read-and-confirmed
```
---
## Problem
In the **Signature Certificate report** (`Signierungszertifikat`), the history event
list always showed **"Document signed"** for status `DocumentSigned` (code `2005`) <20>
even when the envelope type was **"Read and Sign"** (`EnvelopeTypeId = 2`).
For "Read and Sign" envelopes the correct label must be **"Read and confirmed"**
(DE: **"Gelesen und best<73>tigt"**).
### Screenshot reference
The report shows a table with columns *Ereignis | Benutzer | Zeitstempel*.
The row with status 2005 displayed "Document signed" where "Read and confirmed"
was expected.
---
## Root Cause
`ReportItem.ItemStatusTranslated` in
`EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb`
resolved the display label by calling:
```vb
My.Resources.Model.ResourceManager.GetString(ItemStatus.ToString())
```
There was **no branching logic** to handle the Read and Sign envelope type.
The same `DocumentSigned` key was always used regardless of envelope type.
---
## Solution
### Convention followed
The project already uses a `RaC` (**R**ead **a**nd **C**onfirm) suffix for
Read-and-Sign label variants across all resource files:
| Existing RaC keys |
|---|
| `DocumentRejectedRaC` |
| `EnvelopeReportCreatedRaC` |
| `MessageConfirmationSentRaC` |
### New resource key added: `DocumentSignedRaC`
| Resource file | Value |
|---|---|
| `Model.resx` (DE, default) | `Gelesen und best<73>tigt` |
| `Model.en.resx` (EN) | `Read and confirmed` |
### Logic change in `ReportItem.vb`
```vb
Public ReadOnly Property ItemStatusTranslated As String
Get
Dim oStatusKey = ItemStatus.ToString()
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
Envelope IsNot Nothing AndAlso
Envelope.IsReadAndConfirm() Then
oStatusKey = "DocumentSignedRaC"
End If
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
End Get
End Property
```
`Envelope.IsReadAndConfirm()` is defined in
`EnvelopeGenerator.Domain\Interfaces\IEnvelope.cs` and returns `true`
when `EnvelopeTypeId == 2`.
---
## Files Changed
| # | File | Change | Status |
|---|---|---|---|
| 1 | `EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb` | Added `RaC` branching logic to `ItemStatusTranslated` | ? Done |
| 2 | `EnvelopeGenerator.Domain\Resources\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
| 3 | `EnvelopeGenerator.CommonServices\Strings\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
| 4 | `EnvelopeGenerator.CommonServices\Strings\Model.en.resx` | Added `DocumentSignedRaC` = `Read and confirmed` | ✅ Done |
| 5 | `EnvelopeGenerator.Domain\Resources\Model.Designer.cs` | Added `DocumentSignedRaC` strongly-typed property | ✅ Done |
---
## How to resume (for next agent)
All pending items are **resource file additions only**.
Each file needs one new `<data>` block inserted **directly after** the existing
`DocumentSigned` entry:
```xml
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Gelesen und best<73>tigt</value> <!-- DE files -->
<!-- OR -->
<value>Read and confirmed</value> <!-- EN file -->
</data>
```
`Model.Designer.cs` also needs a new strongly-typed property added after
`DocumentSigned`:
```csharp
/// <summary>
/// Looks up a localized string similar to Gelesen und best<73>tigt.
/// </summary>
public static string DocumentSignedRaC {
get {
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
}
}
```
---
## Testing
After changes, generate a Signature Certificate report for a **Read and Sign**
envelope and verify:
- History row with status 2005 shows **"Read and confirmed"** (EN)
- History row with status 2005 shows **"Gelesen und best<73>tigt"** (DE)
- Regular (non-RaC) envelopes still show **"Document signed"** / **"Dokument unterzeichnet"**

View File

@@ -1,111 +0,0 @@
# Signature Field Formatting — WISAG Feedback
## Context
Customer feedback received from the new WISAG signFLOW project lead (email dated 3/7/2026).
The signature stamp rendered inside the finalized PDF has layout issues that need to be addressed.
### Current State (screenshot from customer email)
```
Signiert von ___________
[ signature image ]
Armin Schneider
C6B85E96A520C60... ← certificate hash, too close to name
Bereichsleiter Kern- ← position text wraps, no spacing to date
Anwendungen
Frankfurt am Main
23.06.2026, 17:13:42 GMT+2
```
### Issues Reported
1. **Spacing between name and certificate number** is too small — needs more vertical gap.
2. **Spacing between position/title and date/time** is too small — should be dynamic (expand with content).
3. *(Secondary, out of scope here)* Multiple signature fields side-by-side scale individually, causing uneven heights.
---
## Agreed Solution
> **"Height stays fixed. Text stretches horizontally. Beyond a certain character limit, truncate with '...'"**
> *(Original: "Höhe bleibt fix. Text dehnt sich horizontal aus. Ab einem bestimmten Limit wird mit '...' abgekürzt.")*
### Rationale
- The signature field dimensions are **fixed** (`1.77" × 1.96"`). Allowing text to wrap causes position and date rows to collide vertically.
- Truncating long strings with ellipsis (`...`) keeps the layout deterministic and prevents overflow.
- Per-field max-length limits allow tuning each text row independently (name, certificate, position, city, date).
---
## Implementation Plan
### 1. `PDFBurnerParams.vb` — Add per-field max-length dictionary
Add a new property `TextMaxLengths` that maps each field name (matching `EGName` keys) to a maximum character count.
```vbnet
' EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb
Public Property TextMaxLengths As New Dictionary(Of String, Integer) From {
{"name", 28},
{"position", 32},
{"city", 24},
{"date", 28}
}
```
Default values are starting points and should be tuned against real customer data.
### 2. `PDFBurner.vb` — Add truncation helper and apply in `AddFormFieldValue`
#### 2a. Add a private helper function
```vbnet
Private Function TruncateText(value As String, fieldName As String) As String
If String.IsNullOrEmpty(value) Then Return value
Dim maxLen As Integer = 0
If _pdfBurnerParams.TextMaxLengths IsNot Nothing AndAlso
_pdfBurnerParams.TextMaxLengths.TryGetValue(fieldName, maxLen) AndAlso
maxLen > 0 AndAlso value.Length > maxLen Then
Return value.Substring(0, maxLen - 3) & "..."
End If
Return value
End Function
```
#### 2b. Apply truncation in the `egName`-aware overload
In `AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)`:
```vbnet
' Before passing value to AddTextAnnot, truncate:
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
```
#### 2c. Apply truncation in the element-based overload (optional)
In `AddFormFieldValue(x, y, width, height, page, value)` — if a field name is available in calling context, pass it through; otherwise this path does not use `egName` and truncation can be skipped or applied with a generic fallback key.
---
## Affected Files
| File | Change |
|------|--------|
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb` | Add `TextMaxLengths As Dictionary(Of String, Integer)` |
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurner.vb` | Add `TruncateText()` helper; apply in `AddFormFieldValue` overloads |
---
## Notes
- Field names (`"name"`, `"position"`, `"city"`, `"date"`) must match the keys used in `EGName.Index` dictionary (`PDFBurner.vb:443–450`).
- The certificate hash field (e.g. `C6B85E96A520C60...`) is already truncated upstream — verify its `egName` mapping if it needs its own limit.
- The `YOffset` / `TopMargin` values in `PDFBurnerParams` may also need adjustment to increase vertical spacing between rows. That is a separate (simpler) change.
- This issue is **not** related to the Blazor WebUI rendering — it is purely in the GdPicture14 PDF burning pipeline.