Files
TekH 35c1374b9c Update text truncation logic to use rendered width
Replaced character-based truncation with pixel-accurate truncation based on rendered width in inches. Updated `TruncateText` in `PDFBurner.vb` to use `Graphics.MeasureString` for width calculation and introduced a binary search for efficient truncation. Renamed `egName` to `fieldEgName` for clarity.

Replaced `TextMaxLengths` with `TextMaxWidths` in `PDFBurnerParams.vb`, specifying maximum render widths in inches. Updated default values for `position`, `city`, and `date`. Revised XML documentation to reflect the new truncation logic.
2026-08-25 09:47:22 +02:00

514 lines
21 KiB
VB.net

Imports System.Collections.Immutable
Imports System.Drawing
Imports System.IO
Imports DevExpress.DataProcessing
Imports DigitalData.Core.Abstraction.Application.Repository
Imports DigitalData.Core.Abstractions
Imports DigitalData.Modules.Base
Imports DigitalData.Modules.Logging
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions
Imports EnvelopeGenerator.Domain.Entities
Imports EnvelopeGenerator.PdfEditor
Imports GdPicture14
Imports Microsoft.EntityFrameworkCore
Imports Newtonsoft.Json
Namespace Jobs.FinalizeDocument
Public Class PDFBurner
Inherits BaseClass
Private ReadOnly Manager As AnnotationManager
Private ReadOnly LicenseManager As LicenseManager
Private Property _pdfBurnerParams As PDFBurnerParams
Public Sub New(pLogConfig As LogConfig, pGDPictureLicenseKey As String, pdfBurnerParams As PDFBurnerParams)
MyBase.New(pLogConfig)
LicenseManager = New LicenseManager()
LicenseManager.RegisterKEY(pGDPictureLicenseKey)
Manager = New AnnotationManager()
_pdfBurnerParams = pdfBurnerParams
End Sub
#Region "Burn PDF"
Public Function BurnAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String), envelopeId As Integer) As Byte()
'read the elements of envelope with their annotations
Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If envelope Is Nothing Then
Throw New BurnAnnotationException($"Envelope with Id {envelopeId} not found.")
ElseIf envelope.ReadOnly Then
Return pSourceBuffer
End If
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
.Include(Function(sig) sig.Annotations) _
.ToList()
Return If(elements.Any(),
BurnElementAnnotsToPDF(pSourceBuffer, elements),
BurnInstantJSONAnnotsToPDF(pSourceBuffer, pInstantJSONList))
End Using
End Function
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of DocReceiverElement)) As Byte()
' Add background
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
'TODO: take the length from the largest y
pSourceBuffer = doc.Background(elements, 1.9500000000000002 * 0.93, 2.52 * 0.67).ExportStream().ToArray()
Dim oResult As GdPictureStatus
Using oSourceStream As New MemoryStream(pSourceBuffer)
' Open PDF
oResult = Manager.InitFromStream(oSourceStream)
If oResult <> GdPictureStatus.OK Then
Throw New BurnAnnotationException($"Could not open document for burning: [{oResult}]")
End If
' Imported from background (add to configuration)
Dim margin As Double = 0.2
Dim inchFactor As Double = 72
' Y offset of form fields
Dim keys = {"position", "city", "date"} ' add to configuration
Dim unitYOffsets = 0.2
Dim yOffsetsOfFF = keys.
Select(Function(k, i) New With {Key .Key = k, Key .Value = unitYOffsets * i + 1}).
ToDictionary(Function(x) x.Key, Function(x) x.Value)
'Add annotations
For Each element In elements
If element Is Nothing Then
Continue For
End If
Dim elementAnnotations = If(element.Annotations, Enumerable.Empty(Of ElementAnnotation)())
If Not elementAnnotations.Any() Then
Continue For
End If
Dim frameX = (element.Left - 0.7 - margin)
Dim frame = elementAnnotations.FirstOrDefault(Function(a) a.Name = "frame")
Dim frameY = element.Top - 0.5 - margin
Dim frameYShift As Double = 0
Dim frameXShift As Double = 0
If frame IsNot Nothing Then
frameYShift = frame.Y - frameY * inchFactor
frameXShift = frame.X - frameX * inchFactor
End If
For Each annot In elementAnnotations
If annot Is Nothing Then
Continue For
End If
Dim yOffsetofFF As Double = 0
If Not String.IsNullOrEmpty(annot.Name) Then
yOffsetsOfFF.TryGetValue(annot.Name, yOffsetofFF)
End If
Dim y = frameY + yOffsetofFF
If annot.Type = AnnotationType.FormField Then
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
ElseIf annot.Type = AnnotationType.Image Then
AddImageAnnotation(
annot.X / inchFactor,
If(annot.Name = "signature", (annot.Y - frameYShift) / inchFactor, y),
annot.Width / inchFactor,
annot.Height / inchFactor,
element.Page,
annot.Value
)
ElseIf annot.Type = AnnotationType.Ink Then
AddInkAnnotation(element.Page, annot.Value)
End If
Next
Next
'Save PDF
Using oNewStream As New MemoryStream()
oResult = Manager.SaveDocumentToPDF(oNewStream)
If oResult <> GdPictureStatus.OK Then
Throw New BurnAnnotationException($"Could not save document to stream: [{oResult}]")
End If
Manager.Close()
Return oNewStream.ToArray()
End Using
End Using
End Using
End Function
Public Function BurnInstantJSONAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String)) As Byte()
Dim oResult As GdPictureStatus
Using oSourceStream As New MemoryStream(pSourceBuffer)
' Open PDF
oResult = Manager.InitFromStream(oSourceStream)
If oResult <> GdPictureStatus.OK Then
Throw New BurnAnnotationException($"Could not open document for burning: [{oResult}]")
End If
' Add annotation to PDF
For Each oJSON In pInstantJSONList
Try
AddInstantJSONAnnotationToPDF(oJSON)
Catch ex As Exception
Logger.Warn($"Error in AddInstantJSONAnnotationToPDF - oJson: ")
Logger.Warn(oJSON)
Throw New BurnAnnotationException($"Adding Annotation failed", ex)
End Try
Next
oResult = Manager.BurnAnnotationsToPage(RemoveInitialAnnots:=True, VectorMode:=True)
If oResult <> GdPictureStatus.OK Then
Throw New BurnAnnotationException($"Could not burn annotations to file: [{oResult}]")
End If
'Save PDF
Using oNewStream As New MemoryStream()
oResult = Manager.SaveDocumentToPDF(oNewStream)
If oResult <> GdPictureStatus.OK Then
Throw New BurnAnnotationException($"Could not save document to stream: [{oResult}]")
End If
Manager.Close()
Return oNewStream.ToArray()
End Using
End Using
End Function
#End Region
#Region "Add Value"
Private Sub AddInstantJSONAnnotationToPDF(pInstantJSON As String)
Dim oAnnotationData = JsonConvert.DeserializeObject(Of AnnotationData)(pInstantJSON)
oAnnotationData.annotations.Reverse()
For Each oAnnotation In oAnnotationData.annotations
Logger.Debug("Adding AnnotationID: " + oAnnotation.id)
Select Case oAnnotation.type
Case AnnotationType.Image
AddImageAnnotation(oAnnotation, oAnnotationData.attachments)
Exit Select
Case AnnotationType.Ink
AddInkAnnotation(oAnnotation)
Exit Select
Case AnnotationType.Widget
'Add form field values
Dim formFieldValue = oAnnotationData.formFieldValues.FirstOrDefault(Function(fv) fv.name = oAnnotation.id)
If formFieldValue IsNot Nothing AndAlso Not _pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value) Then
AddFormFieldValue(oAnnotation, formFieldValue)
End If
Exit Select
End Select
Next
End Sub
Private Sub AddImageAnnotation(x As Double, y As Double, width As Double, height As Double, page As Integer, base64 As String)
Manager.SelectPage(page)
Manager.AddEmbeddedImageAnnotFromBase64(base64, x, y, width, height)
End Sub
Private Sub AddImageAnnotation(pAnnotation As Annotation, pAttachments As Dictionary(Of String, Attachment))
Dim oAttachment = pAttachments.Where(Function(a) a.Key = pAnnotation.imageAttachmentId).
SingleOrDefault()
' Convert pixels to Inches
Dim oBounds = pAnnotation.bbox.Select(AddressOf ToInches).ToList()
Dim oX = oBounds.Item(0)
Dim oY = oBounds.Item(1)
Dim oWidth = oBounds.Item(2)
Dim oHeight = oBounds.Item(3)
Manager.SelectPage(pAnnotation.pageIndex + 1)
Manager.AddEmbeddedImageAnnotFromBase64(oAttachment.Value.binary, oX, oY, oWidth, oHeight)
End Sub
Private Sub AddInkAnnotation(page As Integer, value As String)
Dim ink = JsonConvert.DeserializeObject(Of Ink)(value)
Dim oSegments = ink.lines.points
Dim oColor = ColorTranslator.FromHtml(ink.strokeColor)
Manager.SelectPage(page)
For Each oSegment As List(Of List(Of Single)) In oSegments
Dim oPoints = oSegment.
Select(AddressOf ToPointF).
ToArray()
Manager.AddFreeHandAnnot(oColor, oPoints)
Next
End Sub
Private Sub AddInkAnnotation(pAnnotation As Annotation)
Dim oSegments = pAnnotation.lines.points
Dim oColor = ColorTranslator.FromHtml(pAnnotation.strokeColor)
Manager.SelectPage(pAnnotation.pageIndex + 1)
For Each oSegment As List(Of List(Of Single)) In oSegments
Dim oPoints = oSegment.
Select(AddressOf ToPointF).
ToArray()
Manager.AddFreeHandAnnot(oColor, oPoints)
Next
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 = "")
Manager.SelectPage(page)
' Truncate if needed
Dim displayValue As String = TruncateText(value, egName)
' Add the text annotation
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
' Set the font properties
ant.FontName = _pdfBurnerParams.FontName
ant.FontSize = _pdfBurnerParams.FontSize
ant.FontStyle = _pdfBurnerParams.FontStyle
Manager.SaveAnnotationsToPage()
End Sub
Private Sub AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)
Dim ffIndex As Integer = EGName.Index(pAnnotation.egName)
' Convert pixels to Inches
Dim oBounds = pAnnotation.bbox.Select(AddressOf ToInches).ToList()
Dim oX = oBounds.Item(0)
Dim oY = oBounds.Item(1) + _pdfBurnerParams.YOffset * ffIndex + _pdfBurnerParams.TopMargin
Dim oWidth = oBounds.Item(2)
Dim oHeight = oBounds.Item(3)
Manager.SelectPage(pAnnotation.pageIndex + 1)
' Add the text annotation (truncate if needed)
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
' Set the font properties
ant.FontName = _pdfBurnerParams.FontName
ant.FontSize = _pdfBurnerParams.FontSize
ant.FontStyle = _pdfBurnerParams.FontStyle
Manager.SaveAnnotationsToPage()
End Sub
#End Region
#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
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
Return New PointF(oPoints.Item(0), oPoints.Item(1))
End Function
Private Function ToInches(pValue As Double) As Double
Return pValue / 72
End Function
Private Function ToInches(pValue As Single) As Single
Return pValue / 72
End Function
#End Region
#Region "Model"
Friend Class AnnotationType
Public Const Image As String = "pspdfkit/image"
Public Const Ink As String = "pspdfkit/ink"
Public Const Widget As String = "pspdfkit/widget"
Public Const FormField As String = "pspdfkit/form-field-value"
End Class
Friend Class AnnotationData
Public Property annotations As List(Of Annotation)
Public ReadOnly Property AnnotationsByReceiver As IEnumerable(Of List(Of Annotation))
Get
Return annotations _
.Where(Function(annot) annot.hasStructuredID).ToList() _
.GroupBy(Function(a) a.receiverId) _
.Select(Function(g) g.ToList())
End Get
End Property
Public ReadOnly Property UnstructuredAnnotations As IEnumerable(Of List(Of Annotation))
Get
Return annotations _
.Where(Function(annot) Not annot.hasStructuredID).ToList() _
.GroupBy(Function(a) a.receiverId) _
.Select(Function(g) g.ToList())
End Get
End Property
Public Property attachments As Dictionary(Of String, Attachment)
Public Property formFieldValues As List(Of FormFieldValue)
End Class
Friend Class Annotation
Private _id As String = Nothing
Public envelopeId As Integer = Nothing
Public receiverId As Integer = Nothing
Public index As Integer = Nothing
Public egName As String = PDFBurner.EGName.NoName
Public hasStructuredID As Boolean = False
Public ReadOnly Property isLabel As Boolean
Get
If String.IsNullOrEmpty(egName) Then
Return False
End If
Dim parts As String() = egName.Split("_"c)
Return parts.Length > 1 AndAlso parts(1) = "label"
End Get
End Property
Public Property id As String
Get
Return _id
End Get
Set(value As String)
_id = value
If String.IsNullOrWhiteSpace(_id) Then
Throw New BurnAnnotationException("The identifier of annotation is null or empty.")
End If
Dim parts As String() = value.Split("#"c)
If (parts.Length <> 4) Then
Return
'Throw New BurnAnnotationException($"The identifier of annotation has more or less than 4 sub-part. Id: {_id}")
End If
If Not Integer.TryParse(parts(0), envelopeId) Then
Throw New BurnAnnotationException($"The envelope ID of annotation is not integer. Id: {_id}")
End If
If Not Integer.TryParse(parts(1), receiverId) Then
Throw New BurnAnnotationException($"The receiver ID of annotation is not integer. Id: {_id}")
End If
If Not Integer.TryParse(parts(2), index) Then
Throw New BurnAnnotationException($"The index of annotation is not integer. Id: {_id}")
End If
egName = parts(3)
hasStructuredID = True
End Set
End Property
Public Property bbox As List(Of Double)
Public Property type As String
Public Property isSignature As Boolean
Public Property imageAttachmentId As String
Public Property lines As Lines
Public Property pageIndex As Integer
Public Property strokeColor As String
End Class
Friend Class Ink
Public Property lines As Lines
Public Property strokeColor As String
End Class
Public Class EGName
Public Shared ReadOnly NoName As String = Guid.NewGuid().ToString()
Public Shared ReadOnly Seal As String = "signature"
Public Shared ReadOnly Index As ImmutableDictionary(Of String, Integer) =
New Dictionary(Of String, Integer) From {
{NoName, 0},
{Seal, 0},
{"position", 1},
{"city", 2},
{"date", 3}
}.ToImmutableDictionary()
End Class
Friend Class Lines
Public Property points As List(Of List(Of List(Of Single)))
End Class
Friend Class Attachment
Public Property binary As String
Public Property contentType As String
End Class
Friend Class FormFieldValue
Public Property name As String
Public Property value As String
End Class
#End Region
End Class
End Namespace