Enhanced documentation for the EnvelopeGenerator system: - Added purpose section describing its functionality as a digital document signing system. - Documented solution structure, listing projects, frameworks, and purposes. - Highlighted key files in `ReceiverUI` and `API` with their roles. - Explained `AnnotationDto` coordinate system and unit conversions. - Detailed the signing flow in `ReceiverUI`, including page load, signature popup, and export steps. - Documented JavaScript checkbox overlay logic for annotations. - Provided a breakdown of `StampSignaturesOnPdf` using iText7. - Described `BuildFreshBaseReport` behavior in real and dev modes. - Listed NuGet packages with versions and purposes. - Added "Mistakes History" to prevent repeated errors. - Clarified why DevExpress X.509 signing article does not apply. - Summarized development sessions in a change log.
7.6 KiB
7.6 KiB
EnvelopeGenerator — Copilot Context Notes (English)
Purpose
A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF.
Solution Structure
| Project | Target | Description |
|---|---|---|
EnvelopeGenerator.API |
net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. |
EnvelopeGenerator.ReceiverUI |
net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. |
EnvelopeGenerator.Web |
net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. |
EnvelopeGenerator.Application |
multi | MediatR CQRS handlers. |
EnvelopeGenerator.Domain |
multi | Domain models, constants, interfaces. |
EnvelopeGenerator.Infrastructure |
multi | EF Core repos, DB context. |
EnvelopeGenerator.PdfEditor |
multi | iText7 utilities. NOT used in ReceiverUI flow. |
EnvelopeGenerator.DependencyInjection |
multi | DI registration helpers. |
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
Key Files
| File | Purpose |
|---|---|
ReceiverUI/Pages/ReportViewer.razor |
Main receiver page. All signing logic. |
ReceiverUI/wwwroot/js/receiver-signature.js |
JS: checkbox overlay, signature pad (draw/type/image). |
ReceiverUI/wwwroot/fake-data/annotations.json |
Dev-mode fake annotations (YARP proxy target). |
ReceiverUI/Models/AnnotationDto.cs |
Annotation position model. All properties non-nullable. |
ReceiverUI/Services/AnnotationService.cs |
Fetches List<AnnotationDto> from API or fake-data. |
ReceiverUI/Services/DocumentService.cs |
Fetches PDF bytes from API. |
ReceiverUI/Services/AuthService.cs |
Manages receiver session cookie. |
API/Controllers/AnnotationController.cs |
GET api/Annotation/{key} ? annotation list. |
API/Controllers/DocumentController.cs |
GET api/Document/{key} ? PDF bytes. |
AnnotationDto — Coordinate System
Unit : 1/100 inch (DX units) — DevExpress XtraReports native
Origin : Top-left corner of page
X : increases rightward
Y : increases downward
A4 in DX units: Width = 827, Height = 1169
Conversions:
PSPDFKit (pt, top-left): xDX = xPsPdf * (100/72)
GDPicture (pt, bottom-left): yDX = (pageHeightPt - yGD - elemHeightPt) * (100/72)
DX ? PDF points: pt = dx * (72/100)
ReceiverUI Signing Flow (ReportViewer.razor)
On Load (OnInitializedAsync)
AuthService.CheckEnvelopeAccessAsync? redirect to login if unauthorizedAnnotationService.GetAnnotationsAsync? fills_annotationsDocumentService.GetDocumentAsync? fills_basePdfBytes(real mode)BuildFreshBaseReport()?XtraReportforDxReportViewer
Signature Popup
- Tabs: Draw / Text / Image
- Fields: full name (required), position (optional), place (required)
- Saved to
_capturedSignaturerecord - If annotations exist ? popup closes ? JS checkbox overlays installed
JS Checkbox Overlay (receiver-signature.js)
receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)- One
.annot-sig-cb-wrapperdiv per annotation, absolutely positioned over viewer scroll container - Position:
left = pageRect.left + ann.x * scaleX,top = pageRect.top + ann.y * scaleYscaleX = pagePixelWidth / 827,scaleY = pagePixelHeight / 1169
- Click ?
dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)
Apply Signatures ("Unterschriften anwenden" — SubmitSignaturesAsync)
Real PDF mode (_basePdfBytes is set):
- Calls
StampSignaturesOnPdfusing iText7 directly on PDF bytes - Coordinate conversion:
xPt = ann.X * (72/100),yPt = pageHeight - ann.Y * (72/100) - sigHeight(Y flip: DX top-down ? PDF bottom-up) - Returns stamped bytes ? loaded into new
XtraReportwithXRPdfContent - Viewer refreshed with
ViewerKey++
Dev/fake mode (_basePdfBytes is null):
- Falls back to
AddSignatureAtAnnotation(XtraReports DetailBand + BeforePrint counter) - This is intentionally left as a dev fallback only
Export
reportViewer.ExportToAsync(ExportFormat.Pdf)
StampSignaturesOnPdf — iText7 Implementation
// Located in ReportViewer.razor @code section
// Called by SubmitSignaturesAsync when _basePdfBytes is available
static byte[] StampSignaturesOnPdf(
byte[] sourcePdfBytes, byte[] signatureImageBytes,
string signerFullName, string signerPosition, string signaturePlace,
IReadOnlyList<AnnotationDto> annotations)
{
// Opens PDF with PdfReader/PdfWriter
// For each annotation:
// pageNum = ann.Page (clamped to totalPages)
// xPt = ann.X * (72f/100f)
// imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip
// PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false)
// Separator line at bottom of image
// Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87))
// Returns stamped byte[]
}
BuildFreshBaseReport()
// Real PDF mode:
var report = new XtraReport();
var detail = new DetailBand();
report.Bands.Add(detail);
detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
return report;
// Dev/fake mode: returns pre-built report from ReportStorage
NuGet Packages in ReceiverUI
| Package | Version | Purpose |
|---|---|---|
DevExpress.Blazor.Reporting.Viewer |
25.2.3 | DxReportViewer component |
DevExpress.Blazor.PdfViewer |
25.2.3 | PDF viewer |
DevExpress.Drawing.Skia |
25.2.3 | Drawing backend |
itext |
8.0.5 | PDF stamping (iText7) |
SkiaSharp.* |
3.119.1 | WASM native rendering |
Mistakes History — Do NOT Repeat
| Mistake | Why Wrong |
|---|---|
BottomMarginBand for per-page signatures |
Repeats on every page; Y offset wrong |
imageY = (page-1) * 1169 + ann.Y |
Inflates DetailBand; 35 pages ? 140 pages |
e.Graph?.PrintingSystem in BeforePrint |
Graph not on CancelEventArgs |
ctrl.Report?.PrintingSystem |
PrintingSystem not on XtraReportBase in WASM |
Adding stamp endpoint to DocumentController |
Not needed; stamping is done client-side in ReceiverUI |
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly |
DevExpress Article (2023-08-28) — Why It Does NOT Apply
The article describes X.509 cryptographic digital signatures via PdfDocumentSigner + Pkcs7Signer.
Our use case is visual/image stamping at specific page coordinates — different problem, different API.
XRPdfSignature in the article requires pre-placed fields in the report designer, not runtime coordinates.
Change Log
| Session | Date | Change |
|---|---|---|
| 1–3 | — | Core infrastructure: services, YARP proxy, JS overlay, signature pad |
| 4 | — | AddSignatureAtAnnotation with BottomMarginBand — ? repeated on all pages |
| 5 | — | BeforePrint + e.Graph?.PrintingSystem — ? compile error |
| 6 | — | BeforePrint counter — ? correct pattern, wrong band |
| 7 | — | Switched to DetailBand — ? correct band |
| 8 | — | (page-1)*1169+Y offset — ? 35?140 page inflation |
| 9 | — | Fixed: BoundsF.Y = ann.Y + counter; created COPILOT_CONTEXT.md |
| 10 | — | Investigated DevExpress article — not applicable to our case |
| 10 | — | Added iText7 to ReceiverUI; implemented StampSignaturesOnPdf — ? deterministic coordinates, no page count side effects |
| 10 | — | Split COPILOT_CONTEXT.md into COPILOT_CONTEXT_EN.md and COPILOT_CONTEXT_TR.md |