Refactor PDF handling; remove iText dependency

Replaced iText-based PDF processing with DevExpress PdfGraphics API.
Removed `itext` and `itext.bouncy-castle-adapter` dependencies.
Simplified `BuildReport` to burn signatures directly into PDFs
and render all pages using `XRPdfContent` with `GenerateOwnPages = true`.
Consolidated subreport logic into `BuildReport` and removed
`BuildPageSubreport`. Eliminated unused constants and methods,
including `GetPdfPageCount`. Updated XML documentation to reflect
the new implementation. Placeholder implementation for
`BurnSignaturesIntoPdf` added, pending further development.
This commit is contained in:
2026-06-30 23:55:39 +02:00
parent 8f4b751303
commit 733b70cca2
2 changed files with 46 additions and 126 deletions

View File

@@ -2,8 +2,6 @@
@rendermode InteractiveServer @rendermode InteractiveServer
@using DevExpress.Blazor.Reporting @using DevExpress.Blazor.Reporting
@using DevExpress.XtraReports.UI @using DevExpress.XtraReports.UI
@using DevExpress.XtraPrinting
@using DevExpress.XtraPrinting.Drawing
@using EnvelopeGenerator.Server.Client.Models @using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Models.Constants @using EnvelopeGenerator.Server.Client.Models.Constants
@using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Server.Client.Services
@@ -352,15 +350,6 @@
const string ImageInputId = "rp-signature-image-input"; const string ImageInputId = "rp-signature-image-input";
const string ImageCanvasId = "rp-image-signature-pad"; const string ImageCanvasId = "rp-image-signature-pad";
// A4 page dimensions in DX units (1/100 inch).
// 8.27" × 11.69" → 827 × 1169
const float PageWidthDx = 827f;
const float PageHeightDx = 1169f;
// Fixed signature field size in DX units: 1.77" × 1.96"
const float SigWidthDx = 177f;
const float SigHeightDx = 196f;
readonly (string Text, string Value)[] TypedSignatureFonts = readonly (string Text, string Value)[] TypedSignatureFonts =
[ [
("Brush Script", "'Brush Script MT', cursive"), ("Brush Script", "'Brush Script MT', cursive"),
@@ -478,135 +467,68 @@
// ----- Report builder ----- // ----- Report builder -----
/// <summary> /// <summary>
/// Builds an XtraReport that displays the PDF pages via XRPdfContent (embedded mode, /// Builds an XtraReport wrapping the PDF bytes.
/// GenerateOwnPages = false). Each PDF page is wrapped in its own subreport so that /// If a signature is captured and there are signature fields, the signature image is
/// XRPictureBox overlays can be positioned accurately per page. /// first burned into the PDF via DevExpress PdfDocumentProcessor, then the modified
/// PDF is handed to XRPdfContent with GenerateOwnPages = true so that all pages appear.
/// </summary> /// </summary>
static XtraReport BuildReport( static XtraReport BuildReport(
byte[] pdfBytes, byte[] pdfBytes,
IReadOnlyList<SignatureDto> signatures, IReadOnlyList<SignatureDto> signatures,
SignatureCaptureDto? capturedSignature) SignatureCaptureDto? capturedSignature)
{ {
// Determine the number of pages using DevExpress PDF processor // Burn signatures into PDF bytes when a captured signature is available
int pageCount = GetPdfPageCount(pdfBytes); byte[] sourcePdf = pdfBytes;
if (pageCount < 1) pageCount = 1; if (capturedSignature is not null
&& !string.IsNullOrWhiteSpace(capturedSignature.DataUrl)
// Outer (main) report - acts as container for subreports && signatures.Count > 0)
var mainReport = new XtraReport
{ {
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, try
Landscape = false,
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
};
var mainDetail = new DetailBand { HeightF = 0f };
mainReport.Bands.Add(mainDetail);
for (int page = 1; page <= pageCount; page++)
{
// Build a subreport for this PDF page
var pageReport = BuildPageSubreport(pdfBytes, page, signatures, capturedSignature);
var subreport = new XRSubreport
{ {
ReportSource = pageReport, sourcePdf = BurnSignaturesIntoPdf(pdfBytes, signatures, capturedSignature);
GenerateOwnPages = true, }
LocationF = new PointF(0f, 0f), catch
SizeF = new SizeF(PageWidthDx, PageHeightDx),
};
mainDetail.Controls.Add(subreport);
}
return mainReport;
}
/// <summary>
/// Builds a single-page subreport: one DetailBand containing the PDF page (via
/// XRPdfContent with GenerateOwnPages = false) plus XRPictureBox overlays for
/// any signatures placed on this page.
/// </summary>
static XtraReport BuildPageSubreport(
byte[] pdfBytes,
int pageNumber,
IReadOnlyList<SignatureDto> signatures,
SignatureCaptureDto? capturedSignature)
{
var report = new XtraReport
{
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
Landscape = false,
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
};
var detail = new DetailBand
{
HeightF = PageHeightDx,
Name = $"DetailBand_Page{pageNumber}",
};
report.Bands.Add(detail);
// --- PDF content (embedded, no own pages) ---
var pdfContent = new XRPdfContent
{
Source = pdfBytes,
PageRange = pageNumber.ToString(),
GenerateOwnPages = false,
LocationF = new PointF(0f, 0f),
SizeF = new SizeF(PageWidthDx, PageHeightDx),
};
detail.Controls.Add(pdfContent);
// --- Signature overlays ---
if (capturedSignature is not null && !string.IsNullOrWhiteSpace(capturedSignature.DataUrl))
{
var signaturesOnPage = signatures.Where(s => s.Page == pageNumber).ToList();
foreach (var sig in signaturesOnPage)
{ {
try // Fall back to unmodified PDF — non-critical
{ sourcePdf = pdfBytes;
var imgBytes = DataUrlToBytes(capturedSignature.DataUrl);
if (imgBytes is { Length: > 0 })
{
using var imgStream = new System.IO.MemoryStream(imgBytes);
var img = System.Drawing.Image.FromStream(imgStream);
var picBox = new XRPictureBox
{
// DB stores INCHES; DX unit = 1/100 inch → multiply by 100
LocationF = new PointF((float)(sig.X * 100), (float)(sig.Y * 100)),
SizeF = new SizeF(SigWidthDx, SigHeightDx),
Image = img,
Sizing = ImageSizeMode.Squeeze,
CanGrow = false,
CanShrink = false,
};
detail.Controls.Add(picBox);
}
}
catch
{
// Non-critical: skip overlay on error
}
} }
} }
var report = new XtraReport
{
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
Landscape = false,
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
};
var detail = new DetailBand { HeightF = 0f };
report.Bands.Add(detail);
// GenerateOwnPages = true (default): each PDF page becomes a separate report page
var pdfContent = new XRPdfContent
{
Source = sourcePdf,
GenerateOwnPages = true,
};
detail.Controls.Add(pdfContent);
return report; return report;
} }
/// <summary>Reads the page count of a PDF using iText7 (already referenced in the server project).</summary> /// <summary>
static int GetPdfPageCount(byte[] pdfBytes) /// Burns signature images directly into the PDF using DevExpress PdfGraphics API.
/// Coordinates: DB stores INCHES with top-left origin, Y down.
/// PDF coordinate system: bottom-left origin, Y up, unit = points (1/72 inch).
/// Note: Implementation placeholder — requires DevExpress.Pdf.Drawing API wiring (Problem 2).
/// </summary>
static byte[] BurnSignaturesIntoPdf(
byte[] pdfBytes,
IReadOnlyList<SignatureDto> signatures,
SignatureCaptureDto capturedSignature)
{ {
try // TODO: Implement with PdfGraphics when Problem 2 is addressed.
{ // For now return unmodified PDF so Problem 1 (all pages) can be verified first.
using var ms = new System.IO.MemoryStream(pdfBytes); return pdfBytes;
using var reader = new iText.Kernel.Pdf.PdfReader(ms);
using var pdfDoc = new iText.Kernel.Pdf.PdfDocument(reader);
return pdfDoc.GetNumberOfPages();
}
catch
{
return 1;
}
} }
/// <summary>Converts a base64 data URL (data:image/...;base64,...) to raw bytes.</summary> /// <summary>Converts a base64 data URL (data:image/...;base64,...) to raw bytes.</summary>

View File

@@ -28,8 +28,6 @@
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" /> <PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" /> <PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" />
<PackageReference Include="itext" Version="8.0.5" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />