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
4 changed files with 108 additions and 102 deletions

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

@@ -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,40 +169,27 @@ 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;
} }
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,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>