diff --git a/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs b/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs
index 808561f6..eedeb896 100644
--- a/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs
+++ b/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs
@@ -6,7 +6,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
///
/// Data Transfer Object representing a positioned element assigned to a document receiver.
///
-public class DocReceiverElementDto : IDocReceiverElement
+public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
{
///
/// Gets or sets the unique identifier of the element.
@@ -122,4 +122,41 @@ public class DocReceiverElementDto : IDocReceiverElement
///
///
public byte[]? Ink { get; set; }
-}
\ No newline at end of file
+
+ ///
+ /// Returns the stored signature bytes as an image payload with detected MIME type.
+ ///
+ /// Thrown when signature image bytes do not exist.
+ 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;
+ }
+}
diff --git a/EnvelopeGenerator.Application/Common/Interfaces/Doc/IPdfPageImageRenderer.cs b/EnvelopeGenerator.Application/Common/Interfaces/Doc/IPdfPageImageRenderer.cs
index 361211f0..8dcbf9b3 100644
--- a/EnvelopeGenerator.Application/Common/Interfaces/Doc/IPdfPageImageRenderer.cs
+++ b/EnvelopeGenerator.Application/Common/Interfaces/Doc/IPdfPageImageRenderer.cs
@@ -1,8 +1,54 @@
+using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
+using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
+///
+///
+///
public interface IPdfPageImageRenderer
{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
Task> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ byte[] ComposeSignaturePreviewPng(ISignatureInkPayload element);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool IsSupportedImageFormat(byte[] bytes);
}
+
+///
+///
+///
+public static class PdfPageImageRendererExtensions
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.Domain/Entities/DocReceiverElement.cs b/EnvelopeGenerator.Domain/Entities/DocReceiverElement.cs
index ed17921d..9dd59387 100644
--- a/EnvelopeGenerator.Domain/Entities/DocReceiverElement.cs
+++ b/EnvelopeGenerator.Domain/Entities/DocReceiverElement.cs
@@ -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()
{
diff --git a/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj b/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj
index fc81b9f0..04fd248f 100644
--- a/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj
+++ b/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj
@@ -2,6 +2,7 @@
net462;net7.0;net8.0;net9.0
+ latest
1.4.0
1.4.0
diff --git a/EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs b/EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs
new file mode 100644
index 00000000..c9a722f9
--- /dev/null
+++ b/EnvelopeGenerator.Domain/Interfaces/ISignatureInkPayload.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs b/EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
index 234397d8..7aa15145 100644
--- a/EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
+++ b/EnvelopeGenerator.Infrastructure.Doc/PdfPageImageRenderer.cs
@@ -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>(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);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
index 25ebac73..e668bbf9 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
@@ -317,11 +317,11 @@
// ----- PDF signature rendering -----
///
- /// 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
///
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;
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DevSignatureInkController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DevSignatureInkController.cs
new file mode 100644
index 00000000..902983b1
--- /dev/null
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DevSignatureInkController.cs
@@ -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;
+
+///
+/// Temporary development endpoint to download stored signature ink image by element id.
+///
+[ApiController]
+[AllowAnonymous]
+[Route("api/dev/signature-ink")]
+public class DevSignatureInkController(
+ IRepository repository,
+ IWebHostEnvironment environment,
+ IConfiguration configuration,
+ IPdfPageImageRenderer renderer) : ControllerBase
+{
+ ///
+ /// Downloads the signature image stored in INK for the given DocumentReceiverElement id.
+ ///
+ [HttpGet("{id:int}")]
+ public async Task 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}");
+ }
+
+ ///
+ /// Downloads a composed signature image (background + ink + signer metadata) for the given element id.
+ ///
+ [HttpGet("{id:int}/composed")]
+ public async Task 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("DevEndpoints:EnableSignatureInkInProduction");
+}
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj
index 03aa3cc3..ea0d6042 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj
@@ -48,7 +48,6 @@
-
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
index ec34e679..51bf6274 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
@@ -2,6 +2,9 @@
"UseSwagger": true,
"UseScalar": true,
"UseDbMigration": false,
+ "DevEndpoints": {
+ "EnableSignatureInkInProduction": false
+ },
"DiPMode": true,
"Logging": {
"LogLevel": {