Files
EnvelopeGenerator/EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
TekH 93142a56f7 Add signature ink handling and DevExpress integration
Introduced `ISignatureInkPayload` interface to standardize signature ink data and metadata. Updated `DocReceiverElementDto` and `DocReceiverElement` to implement this interface. Enhanced `IPdfPageImageRenderer` with methods for signature preview generation and image format validation.

Replaced `PdfSharp` with `DevExpress` for PDF rendering in `ReceiverSignedPage.razor`. Added `DevSignatureInkController` with endpoints for downloading raw and composed signature images. Updated project files to remove `SixLabors.ImageSharp` and integrate DevExpress.

Added configuration for development endpoints in `appsettings.json`. Improved exception handling, validation, and code maintainability.
2026-10-19 14:27:21 +02:00

179 lines
6.8 KiB
C#

using DevExpress.Drawing;
using DevExpress.Pdf;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Infrastructure.Doc;
public class PdfPageImageRenderer : IPdfPageImageRenderer
{
private const float RenderDpi = 110f;
public Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfBytes);
if (pdfBytes.Length == 0)
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
using var stream = new MemoryStream(pdfBytes, writable: false);
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(stream);
var pages = processor.Document.Pages;
if (pages.Count == 0)
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(RenderDpi);
var results = new List<PdfPageImageDto>(pages.Count);
for (var i = 0; i < pages.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var pageNumber = i + 1;
var page = pages[i];
var cropBox = page.CropBox;
using DXBitmap bitmap = processor.CreateDXBitmap(pageNumber, renderOptions);
using var imageStream = new MemoryStream();
bitmap.Save(imageStream, DXImageFormat.Png);
var dataUrl = $"data:image/png;base64,{Convert.ToBase64String(imageStream.ToArray())}";
results.Add(new PdfPageImageDto
{
PageNumber = pageNumber,
WidthPt = cropBox.Width,
HeightPt = cropBox.Height,
WidthPx = bitmap.Width,
HeightPx = bitmap.Height,
ImageDataUrl = dataUrl
});
}
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(results);
}
public byte[] ComposeSignaturePreviewPng(ISignatureInkPayload element)
{
const double sigW = 1.77 * 72;
const double sigH = 1.96 * 72;
const double imgRatio = 0.52;
const double lineH = 11.5;
const double bgPad = 3.0;
const double padding = 3.0;
var date = element.ChangedWhen!.Value.ToString("dd.MM.yyyy");
var position = string.IsNullOrWhiteSpace(element.Position) ? null : element.Position.Trim();
var x = bgPad;
var y = bgPad;
var imgH = sigH * imgRatio;
var lineY = y + imgH + 1.0;
var textY = lineY + 1.5;
var row1Y = textY;
var row2Y = row1Y + lineH;
var row3Y = position is null ? row2Y : row2Y + lineH;
var contentBottom = row3Y + lineH;
var pageWidth = sigW + (bgPad * 2);
var pageHeight = contentBottom + bgPad;
var inkBytes = this.EnsureSupportedImageBytes(element.Ink!);
using var processor = new PdfDocumentProcessor();
processor.CreateEmptyDocument();
var page = processor.AddNewPage(new PdfRectangle(0, 0, pageWidth, pageHeight));
using var graphics = processor.CreateGraphics();
using var bgBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 255, 253, 240));
using var linePen = new DXPen(System.Drawing.Color.FromArgb(180, 100, 100, 120), 0.5f);
using var nameBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 20, 20, 20));
using var infoBrush = new DXSolidBrush(System.Drawing.Color.FromArgb(255, 80, 80, 80));
var fontBold = new DXFont("Arial", 7.5f, DXFontStyle.Bold);
var fontNormal = new DXFont("Arial", 6.5f, DXFontStyle.Regular);
graphics.FillRectangle(bgBrush, new System.Drawing.RectangleF(0, 0, (float)pageWidth, (float)pageHeight));
var imageArea = new System.Drawing.RectangleF((float)x, (float)y, (float)sigW, (float)imgH);
var imageRect = FitImageContain(imageArea, inkBytes);
graphics.DrawImage(inkBytes, imageRect);
graphics.DrawLine(linePen, (float)(x + 2), (float)lineY, (float)(x + sigW - 2), (float)lineY);
graphics.DrawString(
element.FullName!.Trim(),
fontBold,
nameBrush,
new System.Drawing.RectangleF((float)(x + padding), (float)row1Y, (float)(sigW - (padding * 2)), (float)lineH));
if (position is not null)
{
graphics.DrawString(
position,
fontNormal,
infoBrush,
new System.Drawing.RectangleF((float)(x + padding), (float)row2Y, (float)(sigW - (padding * 2)), (float)lineH));
}
graphics.DrawString(
$"{element.Place!.Trim()}, {date}",
fontNormal,
infoBrush,
new System.Drawing.RectangleF((float)(x + padding), (float)row3Y, (float)(sigW - (padding * 2)), (float)lineH));
graphics.AddToPageForeground(page, 72, 72);
using var pdfStream = new MemoryStream();
processor.SaveDocument(pdfStream);
pdfStream.Position = 0;
using var rasterProcessor = new PdfDocumentProcessor();
rasterProcessor.LoadDocument(pdfStream);
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(400f);
using DXBitmap bitmap = rasterProcessor.CreateDXBitmap(1, renderOptions);
using var pngStream = new MemoryStream();
bitmap.Save(pngStream, DXImageFormat.Png);
return pngStream.ToArray();
}
private static System.Drawing.RectangleF FitImageContain(System.Drawing.RectangleF area, byte[] imageBytes)
{
using var imageStream = new MemoryStream(imageBytes, writable: false);
using var image = DXImage.FromStream(imageStream);
if (image.Width <= 0 || image.Height <= 0)
return area;
var scale = Math.Min(area.Width / image.Width, area.Height / image.Height);
var drawW = image.Width * scale;
var drawH = image.Height * scale;
var drawX = area.X + ((area.Width - drawW) / 2f);
var drawY = area.Y + ((area.Height - drawH) / 2f);
return new System.Drawing.RectangleF(drawX, drawY, drawW, drawH);
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public bool IsSupportedImageFormat(byte[] bytes)
{
var isPng = bytes.Length >= 4 &&
bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47;
if (isPng)
return true;
var isJpeg = bytes.Length >= 3 &&
bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF;
return isJpeg;
}
}