This commit is contained in:
OlgunR 2025-04-24 11:43:18 +02:00
commit c961e9fffd
13 changed files with 233 additions and 55 deletions

View File

@ -1,4 +1,5 @@
using EnvelopeGenerator.Web.Models; using EnvelopeGenerator.Web.Models.Annotation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -6,6 +7,7 @@ namespace EnvelopeGenerator.Web.Controllers;
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
[Authorize]
public class ConfigController : ControllerBase public class ConfigController : ControllerBase
{ {
private readonly AnnotationParams _annotParams; private readonly AnnotationParams _annotParams;
@ -18,6 +20,6 @@ public class ConfigController : ControllerBase
[HttpGet("Annotations")] [HttpGet("Annotations")]
public IActionResult GetAnnotationParams() public IActionResult GetAnnotationParams()
{ {
return Ok(_annotParams.AnnotationDictionary); return Ok(_annotParams.AnnotationJSObject);
} }
} }

View File

@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<PackageId>EnvelopeGenerator.Web</PackageId> <PackageId>EnvelopeGenerator.Web</PackageId>
<Version>3.1.1</Version> <Version>3.1.2</Version>
<Authors>Digital Data GmbH</Authors> <Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company> <Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.Web</Product> <Product>EnvelopeGenerator.Web</Product>
@ -13,8 +13,8 @@
<PackageTags>digital data envelope generator web</PackageTags> <PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description> <Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon> <ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<AssemblyVersion>3.1.1</AssemblyVersion> <AssemblyVersion>3.1.2</AssemblyVersion>
<FileVersion>3.1.1</FileVersion> <FileVersion>3.1.2</FileVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright> <Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup> </PropertyGroup>

View File

@ -1,8 +1,8 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Web.Models; namespace EnvelopeGenerator.Web.Models.Annotation;
public record Annotation public record Annotation : IAnnotation
{ {
public required string Name { get; init; } public required string Name { get; init; }
@ -60,6 +60,16 @@ public record Annotation
public Annotation? VerBoundAnnot { get; set; } public Annotation? VerBoundAnnot { get; set; }
#endregion #endregion
public Color? BackgroundColor { get; init; }
#region Border
public Color? BorderColor { get; init; }
public string? BorderStyle { get; init; }
public int? BorderWidth { get; set; }
#endregion
[JsonIgnore] [JsonIgnore]
internal Annotation Default internal Annotation Default
{ {

View File

@ -1,15 +1,21 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Web.Models; namespace EnvelopeGenerator.Web.Models.Annotation;
public class AnnotationParams public class AnnotationParams
{ {
public AnnotationParams()
{
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
}
public Background? Background { get; init; }
#region Annotation
[JsonIgnore] [JsonIgnore]
public Annotation? DefaultAnnotation { get; init; } public Annotation? DefaultAnnotation { get; init; }
private readonly IEnumerable<Annotation> _annots = new List<Annotation>(); private readonly List<Annotation> _annots = new List<Annotation>();
public Annotation this[string name] => _annots.First(a => a.Name == name);
public bool TryGet(string name, out Annotation annotation) public bool TryGet(string name, out Annotation annotation)
{ {
@ -24,34 +30,50 @@ public class AnnotationParams
get => _annots; get => _annots;
init init
{ {
_annots = value; _annots = value.ToList();
if (DefaultAnnotation is not null) if (DefaultAnnotation is not null)
foreach (var annot in _annots) foreach (var annot in _annots)
annot.Default = DefaultAnnotation; annot.Default = DefaultAnnotation;
foreach (var annot in _annots) for (int i = 0; i < _annots.Count; i++)
{ {
#region set bound annotations #region set bound annotations
// horizontal // horizontal
if (annot.HorBoundAnnotName is string horBoundAnnotName) if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
if (TryGet(horBoundAnnotName, out var horBoundAnnot)) if (TryGet(horBoundAnnotName, out var horBoundAnnot))
annot.HorBoundAnnot = horBoundAnnot; _annots[i].HorBoundAnnot = horBoundAnnot;
else else
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined."); throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
// vertical // vertical
if (annot.VerBoundAnnotName is string verBoundAnnotName) if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
if (TryGet(verBoundAnnotName, out var verBoundAnnot)) if (TryGet(verBoundAnnotName, out var verBoundAnnot))
annot.VerBoundAnnot = verBoundAnnot; _annots[i].VerBoundAnnot = verBoundAnnot;
else else
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined."); throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
#endregion #endregion
} }
AnnotationDictionary = _annots.ToDictionary(a => a.Name.ToLower(), a => a);
} }
} }
#endregion
public Dictionary<string, Annotation> AnnotationDictionary { get; private init; } = new(); #region AnnotationJSObject
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
{
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
if (Background is not null)
{
Background.Locate(_annots);
dict.Add(Background.Name.ToLower(), Background);
}
return dict;
}
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
#endregion
} }

View File

@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Web.Models.Annotation;
/// <summary>
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
/// It is only the first annotation as a background for other annotations.
/// </summary>
public record Background : IAnnotation
{
[JsonIgnore]
public double Margin { get; init; }
public string Name { get; } = "Background";
public double? Width { get; set; }
public double? Height { get; set; }
public double Left { get; set; }
public double Top { get; set; }
public Color? BackgroundColor { get; init; }
#region Border
public Color? BorderColor { get; init; }
public string? BorderStyle { get; init; }
public int? BorderWidth { get; set; }
#endregion
public void Locate(IEnumerable<IAnnotation> annotations)
{
// set Top
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
Top = minTop;
// set Left
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
Left = minLeft;
// set Width
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
Width = maxRight - Left;
// set Height
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
Height = maxBottom - Top;
// add margins
Top -= Margin;
Left -= Margin;
Width += Margin * 2;
Height += Margin * 2;
}
}

View File

@ -0,0 +1,10 @@
namespace EnvelopeGenerator.Web.Models.Annotation;
public record Color
{
public int R { get; init; } = 0;
public int G { get; init; } = 0;
public int B { get; init; } = 0;
}

View File

@ -0,0 +1,8 @@
namespace EnvelopeGenerator.Web.Models.Annotation;
public static class Extensions
{
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
}

View File

@ -0,0 +1,22 @@
namespace EnvelopeGenerator.Web.Models.Annotation;
public interface IAnnotation
{
string Name { get; }
double? Width { get; }
double? Height { get; }
double Left { get; }
double Top { get; }
Color? BackgroundColor { get; }
Color? BorderColor { get; }
string? BorderStyle { get; }
int? BorderWidth { get; }
}

View File

@ -15,6 +15,7 @@ using DigitalData.EmailProfilerDispatcher;
using EnvelopeGenerator.Infrastructure; using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Web.Sanitizers; using EnvelopeGenerator.Web.Sanitizers;
using EnvelopeGenerator.Application.Contracts.Services; using EnvelopeGenerator.Application.Contracts.Services;
using EnvelopeGenerator.Web.Models.Annotation;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!"); logger.Info("Logging initialized!");

View File

@ -151,6 +151,21 @@
}, },
"MainPageTitle": null, "MainPageTitle": null,
"AnnotationParams": { "AnnotationParams": {
"Background": {
"Margin": 0.20,
"BackgroundColor": {
"R": 222,
"G": 220,
"B": 215
},
"BorderColor": {
"R": 204,
"G": 202,
"B": 198
},
"BorderStyle": "underline",
"BorderWidth": 4
},
"DefaultAnnotation": { "DefaultAnnotation": {
"Width": 1, "Width": 1,
"Height": 0.5, "Height": 0.5,

View File

@ -5,6 +5,34 @@ async function createAnnotations(document, instance) {
for(var element of document.elements) { for(var element of document.elements) {
const annotParams = await getAnnotationParams(element.left, element.top); const annotParams = await getAnnotationParams(element.left, element.top);
const page = element.page - 1 const page = element.page - 1
//background
if(annotParams.background){
let background = annotParams.background;
const id_background = PSPDFKit.generateInstantId();
const annotation_background = new PSPDFKit.Annotations.WidgetAnnotation({
id: id_background,
pageIndex: page,
formFieldName: id_background,
backgroundColor: background?.backgroundColor ? new PSPDFKit.Color(background.backgroundColor) : null,
blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(background),
fontSize: 8,
borderStyle: background.borderStyle,
borderWidth: background.borderWidth,
borderColor: background?.borderColor ? new PSPDFKit.Color(background.borderColor) : null
});
const formFieldBackground = new PSPDFKit.FormFields.ButtonFormField({
name: id_background,
annotationIds: PSPDFKit.Immutable.List([annotation_background.id]),
value: "",
readOnly: false
});
signatures.push(annotation_background)
signatures.push(formFieldBackground)
}
//signatures //signatures
const id = PSPDFKit.generateInstantId() const id = PSPDFKit.generateInstantId()
@ -12,8 +40,8 @@ async function createAnnotations(document, instance) {
id: id, id: id,
pageIndex: page, pageIndex: page,
formFieldName: id, formFieldName: id,
backgroundColor: PSPDFKit.Color.YELLOW, backgroundColor: PSPDFKit.Color.LIGHT_YELLOW,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.signature), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.signature),
}) })
@ -29,7 +57,7 @@ async function createAnnotations(document, instance) {
pageIndex: page, pageIndex: page,
formFieldName: id_position, formFieldName: id_position,
backgroundColor: PSPDFKit.Color.DarkBlue, backgroundColor: PSPDFKit.Color.DarkBlue,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.position), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.position),
fontSize: 8 fontSize: 8
}) })
@ -48,7 +76,7 @@ async function createAnnotations(document, instance) {
pageIndex: page, pageIndex: page,
formFieldName: id_city, formFieldName: id_city,
backgroundColor: PSPDFKit.Color.DarkBlue, backgroundColor: PSPDFKit.Color.DarkBlue,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.city), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.city),
fontSize: 8 fontSize: 8
}) })
@ -67,7 +95,7 @@ async function createAnnotations(document, instance) {
pageIndex: page, pageIndex: page,
formFieldName: id_date, formFieldName: id_date,
backgroundColor: PSPDFKit.Color.DarkBlue, backgroundColor: PSPDFKit.Color.DarkBlue,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.date), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.date),
fontSize: 8, fontSize: 8,
backgroundColor: PSPDFKit.Color.TRANSPARENT, backgroundColor: PSPDFKit.Color.TRANSPARENT,
@ -97,7 +125,7 @@ async function createAnnotations(document, instance) {
id: id_date_label, id: id_date_label,
pageIndex: page, pageIndex: page,
formFieldName: id_date_label, formFieldName: id_date_label,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.datelabel), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.datelabel),
fontSize: 8, fontSize: 8,
backgroundColor: PSPDFKit.Color.TRANSPARENT, backgroundColor: PSPDFKit.Color.TRANSPARENT,
@ -119,7 +147,7 @@ async function createAnnotations(document, instance) {
id: id_city_label, id: id_city_label,
pageIndex: page, pageIndex: page,
formFieldName: id_city_label, formFieldName: id_city_label,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.citylabel), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.citylabel),
fontSize: 8, fontSize: 8,
backgroundColor: PSPDFKit.Color.TRANSPARENT, backgroundColor: PSPDFKit.Color.TRANSPARENT,
@ -131,7 +159,8 @@ async function createAnnotations(document, instance) {
name: id_city_label, name: id_city_label,
annotationIds: PSPDFKit.Immutable.List([annotation_city_label.id]), annotationIds: PSPDFKit.Immutable.List([annotation_city_label.id]),
value: "Ort", value: "Ort",
readOnly: true readOnly: true,
color: PSPDFKit.Color.BLACK
}) })
//position label //position label
@ -140,7 +169,7 @@ async function createAnnotations(document, instance) {
id: id_position_label, id: id_position_label,
pageIndex: page, pageIndex: page,
formFieldName: id_position_label, formFieldName: id_position_label,
blendMode: 'multiply', blendMode: 'normal',
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.positionlabel), boundingBox: new PSPDFKit.Geometry.Rect(annotParams.positionlabel),
fontSize: 8, fontSize: 8,
backgroundColor: PSPDFKit.Color.TRANSPARENT, backgroundColor: PSPDFKit.Color.TRANSPARENT,

View File

@ -175,21 +175,21 @@ async function setLanguage(language) {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}) })
.then(res => res.json()) .then(res => res.json())
.then(langs => langs.includes(language)) .then(langs => langs.includes(language))
.catch(err => false); .catch(err => false);
if(hasLang) if (hasLang)
return await fetch(`/lang/${language}`, { return await fetch(`/lang/${language}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}) })
.then(response => { .then(response => {
if (response.redirected) if (response.redirected)
window.location.href = response.url; window.location.href = response.url;
else if (!response.ok) else if (!response.ok)
return Promise.reject('Failed to set language'); return Promise.reject('Failed to set language');
}); });
} }
async function logout() { async function logout() {
@ -204,22 +204,23 @@ async function logout() {
}); });
} }
function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
return fetch(`${window.location.origin}/api/Config/Annotations`, { async function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
const annotParams = await fetch(`${window.location.origin}/api/Config/Annotations`, {
credentials: 'include', credentials: 'include',
method: 'GET' method: 'GET'
}) })
.then(res => res.json()) .then(res => res.json());
.then(annotParams => {
for(var key in annotParams){ for (var key in annotParams) {
var annot = annotParams[key]; var annot = annotParams[key];
annot.width *= inchToPointFactor; annot.width *= inchToPointFactor;
annot.height *= inchToPointFactor; annot.height *= inchToPointFactor;
annot.left += leftInInch; annot.left += leftInInch - 0.7;
annot.left *= inchToPointFactor; annot.left *= inchToPointFactor;
annot.top += topInInch; annot.top += topInInch - 0.5;
annot.top *= inchToPointFactor; annot.top *= inchToPointFactor;
} }
return annotParams; return annotParams;
});
} }

View File

@ -1 +1 @@
async function setLangAsync(n,t){document.getElementById("selectedFlag").className="fi "+t+" me-2";await fetch(`/lang/${n}`,{method:"POST",headers:{"Content-Type":"application/json"}})}async function setLanguage(n){const t=await fetch("/lang",{method:"GET",headers:{"Content-Type":"application/json"}}).then(n=>n.json()).then(t=>t.includes(n)).catch(()=>!1);if(t)return await fetch(`/lang/${n}`,{method:"POST",headers:{"Content-Type":"application/json"}}).then(n=>{if(n.redirected)window.location.href=n.url;else if(!n.ok)return Promise.reject("Failed to set language")})}async function logout(){return await fetch(`/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"}}).then(n=>{n.ok&&(window.location.href="/")})}function getAnnotationParams(n=0,t=0,i=72){return fetch(`${window.location.origin}/api/Config/Annotations`,{credentials:"include",method:"GET"}).then(n=>n.json()).then(r=>{var f,u;for(f in r)u=r[f],u.width*=i,u.height*=i,u.left+=n,u.left*=i,u.top+=t,u.top*=i;return r})}class Network{async getEnvelope(n){return this.getRequest(`/api/envelope/${n}`).then(this.wrapJsonResponse.bind(this))}async postEnvelope(n,t,i){return this.postRequest(`/api/envelope/${n}?index=${t}`,i).then(this.wrapJsonResponse.bind(this))}async getDocument(n,t){return this.getRequest(`/api/document/${n}?index=${t}`).then(this.wrapBinaryResponse.bind(this))}async openDocument(n){return this.postRequest(`/api/document/${n}`,{}).then(this.wrapJsonResponse.bind(this))}withCSRFToken(n){const t=getCSRFToken;let i=n.headers;return n.headers={...i,...t},n}getCSRFToken(){const n=document.getElementsByName("__RequestVerificationToken")[0].value;return{"X-XSRF-TOKEN":n}}getRequest(n){const t=this.getCSRFToken(),i={credentials:"include",method:"GET",headers:{...t}};return fetch(n,i)}postRequest(n,t){const i=this.getCSRFToken(),r={credentials:"include",method:"POST",headers:{...i,"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify(t)};return fetch(n,r)}async wrapJsonResponse(n){return await this.wrapResponse(n,async n=>await n.json())}async wrapBinaryResponse(n){return await this.wrapResponse(n,async n=>await n.arrayBuffer())}async wrapResponse(n,t){let i;if(n.status===200){const r=await t(n);i=new WrappedResponse(r,null)}else if(n.status===403){const t=await n.json();i=new WrappedResponse(null,t)}else i=new WrappedResponse(null,null);return i}}class WrappedResponse{constructor(n,t){this.data=n;this.error=t;this.fatal=n===null&&t===null}} async function setLangAsync(n,t){document.getElementById("selectedFlag").className="fi "+t+" me-2";await fetch(`/lang/${n}`,{method:"POST",headers:{"Content-Type":"application/json"}})}async function setLanguage(n){const t=await fetch("/lang",{method:"GET",headers:{"Content-Type":"application/json"}}).then(n=>n.json()).then(t=>t.includes(n)).catch(()=>!1);if(t)return await fetch(`/lang/${n}`,{method:"POST",headers:{"Content-Type":"application/json"}}).then(n=>{if(n.redirected)window.location.href=n.url;else if(!n.ok)return Promise.reject("Failed to set language")})}async function logout(){return await fetch(`/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"}}).then(n=>{n.ok&&(window.location.href="/")})}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await fetch(`${window.location.origin}/api/Config/Annotations`,{credentials:"include",method:"GET"}).then(n=>n.json());for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}class Network{async getEnvelope(n){return this.getRequest(`/api/envelope/${n}`).then(this.wrapJsonResponse.bind(this))}async postEnvelope(n,t,i){return this.postRequest(`/api/envelope/${n}?index=${t}`,i).then(this.wrapJsonResponse.bind(this))}async getDocument(n,t){return this.getRequest(`/api/document/${n}?index=${t}`).then(this.wrapBinaryResponse.bind(this))}async openDocument(n){return this.postRequest(`/api/document/${n}`,{}).then(this.wrapJsonResponse.bind(this))}withCSRFToken(n){const t=getCSRFToken;let i=n.headers;return n.headers={...i,...t},n}getCSRFToken(){const n=document.getElementsByName("__RequestVerificationToken")[0].value;return{"X-XSRF-TOKEN":n}}getRequest(n){const t=this.getCSRFToken(),i={credentials:"include",method:"GET",headers:{...t}};return fetch(n,i)}postRequest(n,t){const i=this.getCSRFToken(),r={credentials:"include",method:"POST",headers:{...i,"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify(t)};return fetch(n,r)}async wrapJsonResponse(n){return await this.wrapResponse(n,async n=>await n.json())}async wrapBinaryResponse(n){return await this.wrapResponse(n,async n=>await n.arrayBuffer())}async wrapResponse(n,t){let i;if(n.status===200){const r=await t(n);i=new WrappedResponse(r,null)}else if(n.status===403){const t=await n.json();i=new WrappedResponse(null,t)}else i=new WrappedResponse(null,null);return i}}class WrappedResponse{constructor(n,t){this.data=n;this.error=t;this.fatal=n===null&&t===null}}