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.
This commit is contained in:
@@ -6,7 +6,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||
/// </summary>
|
||||
public class DocReceiverElementDto : IDocReceiverElement
|
||||
public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the element.
|
||||
@@ -122,4 +122,41 @@ public class DocReceiverElementDto : IDocReceiverElement
|
||||
///
|
||||
/// </summary>
|
||||
public byte[]? Ink { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stored signature bytes as an image payload with detected MIME type.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when signature image bytes do not exist.</exception>
|
||||
public (byte[] Bytes, string ContentType, string FileExtension) ToImage()
|
||||
{
|
||||
if (Ink is not { Length: > 0 })
|
||||
throw new InvalidOperationException("Signature image (Ink) does not exist.");
|
||||
|
||||
if (HasPrefix(Ink, 0x89, 0x50, 0x4E, 0x47))
|
||||
return (Ink, "image/png", ".png");
|
||||
|
||||
if (HasPrefix(Ink, 0xFF, 0xD8, 0xFF))
|
||||
return (Ink, "image/jpeg", ".jpg");
|
||||
|
||||
if (HasPrefix(Ink, 0x52, 0x49, 0x46, 0x46) &&
|
||||
Ink.Length >= 12 &&
|
||||
Ink[8] == 0x57 && Ink[9] == 0x45 && Ink[10] == 0x42 && Ink[11] == 0x50)
|
||||
return (Ink, "image/webp", ".webp");
|
||||
|
||||
return (Ink, "application/octet-stream", ".bin");
|
||||
}
|
||||
|
||||
private static bool HasPrefix(byte[] data, params byte[] prefix)
|
||||
{
|
||||
if (data.Length < prefix.Length)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < prefix.Length; i++)
|
||||
{
|
||||
if (data[i] != prefix[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,54 @@
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IPdfPageImageRenderer
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
byte[] ComposeSignaturePreviewPng(ISignatureInkPayload element);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsSupportedImageFormat(byte[] bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class PdfPageImageRendererExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="renderer"></param>
|
||||
/// <param name="inkBytes"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="BadRequestException"></exception>
|
||||
public static byte[] EnsureSupportedImageBytes(this IPdfPageImageRenderer renderer, byte[] inkBytes)
|
||||
{
|
||||
if (!renderer.IsSupportedImageFormat(inkBytes))
|
||||
throw new BadRequestException("Only PNG and JPEG signature formats are supported.");
|
||||
|
||||
return inkBytes;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ using System.Collections.Generic;
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
{
|
||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||
public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||
public class DocReceiverElement : IDocReceiverElement, ISignatureInkPayload, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||
{
|
||||
public DocReceiverElement()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net462;net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||
<FileVersion>1.4.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
28
EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs
Normal file
28
EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Interfaces
|
||||
{
|
||||
public interface ISignatureInkPayload
|
||||
{
|
||||
#if nullable
|
||||
byte[]? Ink { get; }
|
||||
|
||||
string? FullName { get; }
|
||||
|
||||
string? Position { get; }
|
||||
|
||||
string? Place { get; }
|
||||
#else
|
||||
byte[] Ink { get; }
|
||||
|
||||
string FullName { get; }
|
||||
|
||||
string Position { get; }
|
||||
|
||||
string Place { get; }
|
||||
#endif
|
||||
DateTime? ChangedWhen { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
|
||||
@@ -54,4 +56,124 @@ public class PdfPageImageRenderer : IPdfPageImageRenderer
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -317,11 +317,11 @@
|
||||
// ----- PDF signature rendering -----
|
||||
|
||||
/// <summary>
|
||||
/// Uses PdfSharp to burn the captured signature onto the PDF at each signature field.
|
||||
/// Layout per field (top-left origin, Y down, units = PDF points):
|
||||
/// [top 65%] signature image
|
||||
/// Uses DevExpress PdfGraphics to burn the captured signature onto the PDF at each signature field.
|
||||
/// Layout per field (input coordinates are top-left origin, units = PDF points):
|
||||
/// [top 52%] signature image (aspect-ratio preserved)
|
||||
/// [separator line]
|
||||
/// [bottom 35%] FullName (bold) / Position (optional) / Place, Date
|
||||
/// [bottom] FullName / Position (optional) / Place, Date
|
||||
/// </summary>
|
||||
static byte[] DrawSignaturesOnPdf(
|
||||
byte[] pdfBytes,
|
||||
@@ -331,102 +331,98 @@
|
||||
var imgBytes = DataUrlToBytes(sig.DataUrl);
|
||||
if (imgBytes is not { Length: > 0 }) return pdfBytes;
|
||||
|
||||
using var inputMs = new System.IO.MemoryStream(pdfBytes);
|
||||
using var inputMs = new System.IO.MemoryStream(pdfBytes);
|
||||
using var outputMs = new System.IO.MemoryStream();
|
||||
using var processor = new DevExpress.Pdf.PdfDocumentProcessor();
|
||||
processor.LoadDocument(inputMs);
|
||||
|
||||
var document = PdfSharp.Pdf.IO.PdfReader.Open(
|
||||
inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
|
||||
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;
|
||||
|
||||
const double sigW = 1.77 * 72; // 127.44 pt
|
||||
const double sigH = 1.96 * 72; // 141.12 pt
|
||||
const double imgRatio = 0.52; // top 52% = image
|
||||
const double lineH = 11.5; // fixed row height matching font size (bold 7.5pt + normal 6.5pt)
|
||||
const double bgPad = 3.0; // background box padding around content (pt)
|
||||
|
||||
var black = PdfSharp.Drawing.XColor.FromArgb(255, 20, 20, 20);
|
||||
var darkGray = PdfSharp.Drawing.XColor.FromArgb(255, 80, 80, 80);
|
||||
var lineColor = PdfSharp.Drawing.XColor.FromArgb(180, 100, 100, 120);
|
||||
|
||||
var bgColor = PdfSharp.Drawing.XColor.FromArgb(255, 255, 253, 240);
|
||||
var bgBrush = new PdfSharp.Drawing.XSolidBrush(bgColor);
|
||||
|
||||
var fontBold = new PdfSharp.Drawing.XFont("Arial", 7.5, PdfSharp.Drawing.XFontStyleEx.Bold);
|
||||
var fontNormal = new PdfSharp.Drawing.XFont("Arial", 6.5, PdfSharp.Drawing.XFontStyleEx.Regular);
|
||||
var linePen = new PdfSharp.Drawing.XPen(lineColor, 0.5);
|
||||
|
||||
var fmtLeft = new PdfSharp.Drawing.XStringFormat
|
||||
{
|
||||
Alignment = PdfSharp.Drawing.XStringAlignment.Near,
|
||||
LineAlignment = PdfSharp.Drawing.XLineAlignment.Near,
|
||||
};
|
||||
using var bgBrush = new DevExpress.Drawing.DXSolidBrush(System.Drawing.Color.FromArgb(255, 255, 253, 240));
|
||||
using var linePen = new DevExpress.Drawing.DXPen(System.Drawing.Color.FromArgb(180, 100, 100, 120), 0.5f);
|
||||
using var nameBrush = new DevExpress.Drawing.DXSolidBrush(System.Drawing.Color.FromArgb(255, 20, 20, 20));
|
||||
using var infoBrush = new DevExpress.Drawing.DXSolidBrush(System.Drawing.Color.FromArgb(255, 80, 80, 80));
|
||||
var fontBold = new DevExpress.Drawing.DXFont("Arial", 7.5f, DevExpress.Drawing.DXFontStyle.Bold);
|
||||
var fontNormal = new DevExpress.Drawing.DXFont("Arial", 6.5f, DevExpress.Drawing.DXFontStyle.Regular);
|
||||
|
||||
var date = DateTime.Now.ToString("dd.MM.yyyy");
|
||||
|
||||
foreach (var field in signatures)
|
||||
{
|
||||
int pageIndex = field.Page - 1;
|
||||
if (pageIndex < 0 || pageIndex >= document.PageCount) continue;
|
||||
var pageIndex = field.Page - 1;
|
||||
if (pageIndex < 0 || pageIndex >= processor.Document.Pages.Count)
|
||||
continue;
|
||||
|
||||
var page = document.Pages[pageIndex];
|
||||
var page = processor.Document.Pages[pageIndex];
|
||||
var pageHeight = page.CropBox.Height;
|
||||
|
||||
using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page);
|
||||
var x = field.X;
|
||||
var y = field.Y;
|
||||
|
||||
double x = field.X;
|
||||
double y = field.Y;
|
||||
var imgH = sigH * imgRatio;
|
||||
var lineY = y + imgH + 1.0;
|
||||
var textY = lineY + 1.5;
|
||||
var row1Y = textY;
|
||||
var row2Y = row1Y + lineH;
|
||||
var row3Y = string.IsNullOrWhiteSpace(sig.Position) ? row2Y : row2Y + lineH;
|
||||
var contentBottom = row3Y + lineH;
|
||||
|
||||
// --- Calculate layout positions first (needed for bg rect) ---
|
||||
double imgH = sigH * imgRatio;
|
||||
double lineY = y + imgH + 1.0; // 1pt gap between image and separator
|
||||
double textY = lineY + 1.5; // 1.5pt gap below separator line
|
||||
double padding = 3;
|
||||
using var graphics = processor.CreateGraphics();
|
||||
|
||||
// Row 1: FullName
|
||||
double row1Y = textY;
|
||||
// Row 2: Position (optional)
|
||||
double row2Y = row1Y + lineH;
|
||||
// Row 3: Place, Date — immediately after row2 regardless of position
|
||||
double row3Y = !string.IsNullOrWhiteSpace(sig.Position) ? row2Y + lineH : row2Y;
|
||||
double contentBottom = row3Y + lineH;
|
||||
var bgRect = ToPdfRect(x - bgPad, y - bgPad, sigW + (bgPad * 2), (contentBottom - y) + (bgPad * 2), pageHeight);
|
||||
graphics.FillRectangle(bgBrush, bgRect);
|
||||
|
||||
// --- Background rectangle sized to actual content (not full sigH) ---
|
||||
var bgRect = new PdfSharp.Drawing.XRect(
|
||||
x - bgPad,
|
||||
y - bgPad,
|
||||
sigW + bgPad * 2,
|
||||
(contentBottom - y) + bgPad * 2);
|
||||
gfx.DrawRectangle(bgBrush, bgRect);
|
||||
var imageArea = ToPdfRect(x, y, sigW, imgH, pageHeight);
|
||||
var imageRect = FitImageContain(imageArea, imgBytes);
|
||||
graphics.DrawImage(imgBytes, imageRect);
|
||||
|
||||
// --- Image area ---
|
||||
var imgRect = new PdfSharp.Drawing.XRect(x, y, sigW, imgH);
|
||||
using var imgStream = new System.IO.MemoryStream(imgBytes);
|
||||
var xImg = PdfSharp.Drawing.XImage.FromStream(imgStream);
|
||||
gfx.DrawImage(xImg, imgRect);
|
||||
var lineYPdf = ToPdfY(lineY, pageHeight);
|
||||
graphics.DrawLine(linePen, (float)(x + 2), lineYPdf, (float)(x + sigW - 2), lineYPdf);
|
||||
|
||||
// --- Separator line ---
|
||||
gfx.DrawLine(linePen, x + 2, lineY, x + sigW - 2, lineY);
|
||||
graphics.DrawString(sig.FullName, fontBold, nameBrush, ToPdfRect(x + padding, row1Y, sigW - (padding * 2), lineH, pageHeight));
|
||||
|
||||
// --- Text rows ---
|
||||
// Row 1: FullName (bold)
|
||||
var nameRect = new PdfSharp.Drawing.XRect(x + padding, row1Y, sigW - padding * 2, lineH);
|
||||
gfx.DrawString(sig.FullName, fontBold, new PdfSharp.Drawing.XSolidBrush(black), nameRect, fmtLeft);
|
||||
|
||||
// Row 2: Position (optional)
|
||||
if (!string.IsNullOrWhiteSpace(sig.Position))
|
||||
{
|
||||
var posRect = new PdfSharp.Drawing.XRect(x + padding, row2Y, sigW - padding * 2, lineH);
|
||||
gfx.DrawString(sig.Position, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), posRect, fmtLeft);
|
||||
graphics.DrawString(sig.Position, fontNormal, infoBrush, ToPdfRect(x + padding, row2Y, sigW - (padding * 2), lineH, pageHeight));
|
||||
}
|
||||
|
||||
// Row 3: Place, Date
|
||||
var placeDate = $"{sig.Place}, {date}";
|
||||
var dateRect = new PdfSharp.Drawing.XRect(x + padding, row3Y, sigW - padding * 2, lineH);
|
||||
gfx.DrawString(placeDate, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), dateRect, fmtLeft);
|
||||
graphics.DrawString($"{sig.Place}, {date}", fontNormal, infoBrush, ToPdfRect(x + padding, row3Y, sigW - (padding * 2), lineH, pageHeight));
|
||||
|
||||
graphics.AddToPageForeground(page, 72, 72);
|
||||
}
|
||||
|
||||
document.Save(outputMs);
|
||||
processor.SaveDocument(outputMs);
|
||||
return outputMs.ToArray();
|
||||
}
|
||||
|
||||
static System.Drawing.RectangleF FitImageContain(System.Drawing.RectangleF area, byte[] imageBytes)
|
||||
{
|
||||
using var imageStream = new System.IO.MemoryStream(imageBytes, writable: false);
|
||||
using var image = DevExpress.Drawing.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);
|
||||
}
|
||||
|
||||
static System.Drawing.RectangleF ToPdfRect(double x, double y, double width, double height, double pageHeight)
|
||||
=> new((float)x, (float)(pageHeight - y - height), (float)width, (float)height);
|
||||
|
||||
static float ToPdfY(double y, double pageHeight)
|
||||
=> (float)(pageHeight - y);
|
||||
|
||||
static byte[]? DataUrlToBytes(string? dataUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dataUrl)) return null;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DevExpress.Drawing;
|
||||
using DevExpress.Pdf;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary development endpoint to download stored signature ink image by element id.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
[Route("api/dev/signature-ink")]
|
||||
public class DevSignatureInkController(
|
||||
IRepository<DocReceiverElement> repository,
|
||||
IWebHostEnvironment environment,
|
||||
IConfiguration configuration,
|
||||
IPdfPageImageRenderer renderer) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads the signature image stored in INK for the given DocumentReceiverElement id.
|
||||
/// </summary>
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<IActionResult> DownloadInk([FromRoute] int id, CancellationToken cancel)
|
||||
{
|
||||
if (!IsEndpointEnabled())
|
||||
return NotFound();
|
||||
|
||||
var element = await repository.Query
|
||||
.AsNoTracking()
|
||||
.Where(e => e.Id == id)
|
||||
.Select(e => new DocReceiverElementDto
|
||||
{
|
||||
Id = e.Id,
|
||||
Ink = e.Ink
|
||||
})
|
||||
.SingleOrDefaultAsync(cancel);
|
||||
|
||||
if (element is null)
|
||||
return NotFound($"DocumentReceiverElement not found for id={id}.");
|
||||
|
||||
if (element.Ink is not { Length: > 0 })
|
||||
return NotFound($"No signature image (Ink) found for id={id}.");
|
||||
|
||||
var image = element.ToImage();
|
||||
return File(image.Bytes, image.ContentType, $"signature-ink-{id}{image.FileExtension}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a composed signature image (background + ink + signer metadata) for the given element id.
|
||||
/// </summary>
|
||||
[HttpGet("{id:int}/composed")]
|
||||
public async Task<IActionResult> DownloadComposedInk([FromRoute] int id, CancellationToken cancel)
|
||||
{
|
||||
if (!IsEndpointEnabled())
|
||||
return NotFound();
|
||||
|
||||
var element = await repository.Query
|
||||
.AsNoTracking()
|
||||
.Where(e => e.Id == id)
|
||||
.Select(e => new DocReceiverElementDto
|
||||
{
|
||||
Id = e.Id,
|
||||
Ink = e.Ink,
|
||||
FullName = e.FullName,
|
||||
Position = e.Position,
|
||||
Place = e.Place,
|
||||
ChangedWhen = e.ChangedWhen
|
||||
})
|
||||
.SingleOrDefaultAsync(cancel);
|
||||
|
||||
if (element is null)
|
||||
return NotFound($"DocumentReceiverElement not found for id={id}.");
|
||||
|
||||
if (element.Ink is not { Length: > 0 })
|
||||
return NotFound($"No signature image (Ink) found for id={id}.");
|
||||
|
||||
if (element.ChangedWhen is null)
|
||||
return Conflict($"DocumentReceiverElement id={id} has null ChangedWhen. Element is not signed yet or data integrity is broken.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(element.FullName) || string.IsNullOrWhiteSpace(element.Place))
|
||||
return Conflict($"DocumentReceiverElement id={id} is missing signer metadata (FullName or Place).");
|
||||
|
||||
if (!renderer.IsSupportedImageFormat(element.Ink!))
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType);
|
||||
|
||||
var composedBytes = renderer.ComposeSignaturePreviewPng(element);
|
||||
return File(composedBytes, "image/png", $"signature-composed-{id}.png");
|
||||
}
|
||||
|
||||
private bool IsEndpointEnabled()
|
||||
=> environment.IsDevelopment() || configuration.GetValue<bool>("DevEndpoints:EnableSignatureInkInProduction");
|
||||
}
|
||||
@@ -48,7 +48,6 @@
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="PDFsharp" Version="6.2.4" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
|
||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"UseSwagger": true,
|
||||
"UseScalar": true,
|
||||
"UseDbMigration": false,
|
||||
"DevEndpoints": {
|
||||
"EnableSignatureInkInProduction": false
|
||||
},
|
||||
"DiPMode": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
|
||||
Reference in New Issue
Block a user