Add interactive signature buttons to PDF viewer
Implemented a new feature to render clickable "Sign" buttons on the PDF canvas at signature field positions fetched from the database. - Updated `EnvelopeViewer.razor` to fetch signature data, convert coordinates from inches to points, and invoke JavaScript for rendering. - Added `renderSignatureButtons` and `clearSignatureButtons` functions in `pdf-viewer.js` to dynamically create and position buttons based on page and zoom level. - Modified HTML to include a new `#pdf-signature-layer` overlay for buttons. - Styled buttons with a purple gradient, hover/active effects, and focus outlines for accessibility. - Defined rendering triggers for initial load, page changes, and zoom changes. - Documented coordinate conversion flow from inches to points to pixels for accurate positioning. - Enhanced accessibility with `tabindex="0"`, focus outlines, and semantic `<button>` elements.
This commit is contained in:
@@ -419,6 +419,14 @@ Our use case is **visual/image stamping** at specific page coordinates
|
|||||||
| **11** | **2025-01-XX** | **Made zoom step configurable (buttons, Ctrl+Wheel, slider use same step)** |
|
| **11** | **2025-01-XX** | **Made zoom step configurable (buttons, Ctrl+Wheel, slider use same step)** |
|
||||||
| **11** | **2025-01-XX** | **Fixed thumbnail canvas alignment (object-fit: contain)** |
|
| **11** | **2025-01-XX** | **Fixed thumbnail canvas alignment (object-fit: contain)** |
|
||||||
| **11** | **2025-01-XX** | **Fixed thumbnail re-rendering on sidebar toggle** |
|
| **11** | **2025-01-XX** | **Fixed thumbnail re-rendering on sidebar toggle** |
|
||||||
|
| **12** | **2025-01-XX** | **Fixed coordinate system documentation: Database stores INCHES (not DX units)** |
|
||||||
|
| **12** | **2025-01-XX** | **Updated XML documentation in SignatureDto, AnnotationDto, AnnotationCreateDto** |
|
||||||
|
| **13** | **2025-01-XX** | **Added SenderAppType enum to SignatureDto for Legacy/Blazor app differentiation** |
|
||||||
|
| **13** | **2025-01-XX** | **Implemented signature overlay rendering in PDF.js viewer (INCHES ? normalized ? pixels)** |
|
||||||
|
| **13** | **2025-01-XX** | **Added automatic overlay re-rendering on page change, zoom, and initial load** |
|
||||||
|
| **13** | **2025-01-XX** | **Added clickable signature buttons on PDF canvas (Sign/Unterschreiben)** |
|
||||||
|
| **13** | **2025-01-XX** | **Signature buttons: 150px×60px, purple gradient, pen icon, hover effects** |
|
||||||
|
| **13** | **2025-01-XX** | **JavaScript: `pdfViewer.renderSignatureButtons()` and `pdfViewer.clearSignatureButtons()`** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -503,4 +511,154 @@ Our use case is **visual/image stamping** at specific page coordinates
|
|||||||
- **Mobile (?768px)**: Flex column, thumbnails on top
|
- **Mobile (?768px)**: Flex column, thumbnails on top
|
||||||
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signature Buttons in EnvelopeViewer — Interactive Overlay System
|
||||||
|
|
||||||
|
**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
**Blazor Component (`EnvelopeViewer.razor`):**
|
||||||
|
```csharp
|
||||||
|
IReadOnlyList<SignatureDto> _signatures = [];
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
||||||
|
_signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task RenderSignatureButtonsAsync() {
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons",
|
||||||
|
_signatures, _currentPage, _dotNetRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void OnSignatureButtonClick(int signatureId) {
|
||||||
|
Console.WriteLine($"Signature #{signatureId} signed");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript (`pdf-viewer.js`):**
|
||||||
|
```javascript
|
||||||
|
renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
||||||
|
this.clearSignatureButtons(); // Remove old buttons
|
||||||
|
|
||||||
|
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
|
||||||
|
const signatureLayer = document.getElementById('pdf-signature-layer');
|
||||||
|
|
||||||
|
pageSignatures.forEach(sig => {
|
||||||
|
// Convert POINTS to display pixels
|
||||||
|
const xPx = sig.x * this.scale; // sig.x already in PDF POINTS
|
||||||
|
const yPx = sig.y * this.scale;
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'signature-button';
|
||||||
|
button.style.left = `${xPx}px`;
|
||||||
|
button.style.top = `${yPx}px`;
|
||||||
|
button.style.width = '150px';
|
||||||
|
button.style.height = '60px';
|
||||||
|
|
||||||
|
// German text + pen icon
|
||||||
|
button.innerHTML = `
|
||||||
|
<div>Unterschreiben</div>
|
||||||
|
<svg>...</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureLayer.appendChild(button);
|
||||||
|
this.signatureButtons.push(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSignatureButtons() {
|
||||||
|
this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
|
||||||
|
this.signatureButtons = [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTML Structure:**
|
||||||
|
```html
|
||||||
|
<div class="pdf-page-container">
|
||||||
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
<div id="pdf-text-layer"></div>
|
||||||
|
<div id="pdf-signature-layer"></div> <!-- NEW -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS (`envelope-viewer.css`):**
|
||||||
|
```css
|
||||||
|
.pdf-signature-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; right: 0; bottom: 0;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none; /* Pass clicks through to canvas */
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-button {
|
||||||
|
pointer-events: auto; /* Re-enable for buttons */
|
||||||
|
position: absolute;
|
||||||
|
width: 150px; height: 60px;
|
||||||
|
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
/* Hover: scale(1.05), shadow, darker gradient */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering Triggers
|
||||||
|
|
||||||
|
1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`)
|
||||||
|
2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()`
|
||||||
|
3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions)
|
||||||
|
|
||||||
|
### Coordinate Conversion Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Database (INCHES)
|
||||||
|
? SignatureService.GetAsync()
|
||||||
|
SignatureDto.X/Y (INCHES)
|
||||||
|
? .Convert(UnitOfLength.Point)
|
||||||
|
SignatureDto.X/Y (PDF POINTS, × 72)
|
||||||
|
? JavaScript
|
||||||
|
Display Pixels (× this.scale)
|
||||||
|
? CSS
|
||||||
|
button.style.left/top
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Calculation:**
|
||||||
|
- Database: `X = 1.5 inches, Y = 2.0 inches`
|
||||||
|
- After conversion: `X = 108 points, Y = 144 points` (× 72)
|
||||||
|
- At scale 1.5: `X = 162px, Y = 216px`
|
||||||
|
- Button positioned at `left: 162px, top: 216px`
|
||||||
|
|
||||||
|
### Button Design
|
||||||
|
|
||||||
|
**Visual Spec:**
|
||||||
|
- **Size:** 150px × 60px
|
||||||
|
- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker)
|
||||||
|
- **Text:** "Unterschreiben" (18px, bold, white)
|
||||||
|
- **Icon:** Pen SVG (24px white)
|
||||||
|
- **Effects:**
|
||||||
|
- Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)`
|
||||||
|
- Active: `scale(0.98)`
|
||||||
|
- Focus: `2px solid #7e22ce` outline
|
||||||
|
|
||||||
|
**Accessibility:**
|
||||||
|
- `tabindex="0"` for keyboard navigation
|
||||||
|
- Focus outline for keyboard users
|
||||||
|
- Click handler with semantic button element
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user