mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2024-11-23 15:21:25 +01:00
* Sign multiple PDF pages at the same time in the same location (#2008) * Modifying the functionality of how the signature is added to all pages (#2008) * Adding the functionality to reverse the addition on all pages and implementing buttons to navigate to the first and last pages (#2008)
This commit is contained in:
parent
547f23fe78
commit
204bae3bc1
@ -62,53 +62,54 @@ select#font-select option {
|
|||||||
background-color: rgba(52, 152, 219, 0.2);
|
background-color: rgba(52, 152, 219, 0.2);
|
||||||
/* Darken background on hover */
|
/* Darken background on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-grid {
|
.signature-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list {
|
.signature-list {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list-item {
|
.signature-list-item {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid #dee2e6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list-item:hover {
|
.signature-list-item:hover {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list-info {
|
.signature-list-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list-name {
|
.signature-list-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list-details {
|
.signature-list-details {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-list-details small:not(:last-child) {
|
.signature-list-details small:not(:last-child) {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle {
|
.view-toggle {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ const DraggableUtils = {
|
|||||||
nextId: 0,
|
nextId: 0,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
|
elementAllPages: [],
|
||||||
documentsMap: new Map(),
|
documentsMap: new Map(),
|
||||||
lastInteracted: null,
|
lastInteracted: null,
|
||||||
|
|
||||||
@ -197,6 +198,68 @@ const DraggableUtils = {
|
|||||||
deleteAllDraggableCanvases() {
|
deleteAllDraggableCanvases() {
|
||||||
this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach((el) => el.remove());
|
this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach((el) => el.remove());
|
||||||
},
|
},
|
||||||
|
async addAllPagesDraggableCanvas(element) {
|
||||||
|
if (element) {
|
||||||
|
let currentPage = this.pageIndex
|
||||||
|
if (!this.elementAllPages.includes(element)) {
|
||||||
|
this.elementAllPages.push(element)
|
||||||
|
element.style.filter = 'sepia(1) hue-rotate(90deg) brightness(1.2)';
|
||||||
|
let newElement = {
|
||||||
|
"element": element,
|
||||||
|
"offsetWidth": element.width,
|
||||||
|
"offsetHeight": element.height
|
||||||
|
}
|
||||||
|
|
||||||
|
let pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||||
|
|
||||||
|
if (!pagesMap) {
|
||||||
|
pagesMap = {};
|
||||||
|
this.documentsMap.set(this.pdfDoc, pagesMap);
|
||||||
|
}
|
||||||
|
let page = this.pageIndex
|
||||||
|
|
||||||
|
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
|
||||||
|
|
||||||
|
if (pagesMap[`${pageIndex}-offsetWidth`]) {
|
||||||
|
if (!pagesMap[pageIndex].includes(newElement)) {
|
||||||
|
pagesMap[pageIndex].push(newElement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pagesMap[pageIndex] = []
|
||||||
|
pagesMap[pageIndex].push(newElement)
|
||||||
|
pagesMap[`${pageIndex}-offsetWidth`] = pagesMap[`${page}-offsetWidth`];
|
||||||
|
pagesMap[`${pageIndex}-offsetHeight`] = pagesMap[`${page}-offsetHeight`];
|
||||||
|
}
|
||||||
|
await this.goToPage(pageIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = this.elementAllPages.indexOf(element);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.elementAllPages.splice(index, 1);
|
||||||
|
}
|
||||||
|
element.style.filter = '';
|
||||||
|
let pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||||
|
|
||||||
|
if (!pagesMap) {
|
||||||
|
pagesMap = {};
|
||||||
|
this.documentsMap.set(this.pdfDoc, pagesMap);
|
||||||
|
}
|
||||||
|
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
|
||||||
|
if (pagesMap[`${pageIndex}-offsetWidth`] && pageIndex != currentPage) {
|
||||||
|
const pageElements = pagesMap[pageIndex];
|
||||||
|
pageElements.forEach(elementPage => {
|
||||||
|
const elementIndex = pageElements.findIndex(elementPage => elementPage['element'].id === element.id);
|
||||||
|
if (elementIndex !== -1) {
|
||||||
|
pageElements.splice(elementIndex, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.goToPage(pageIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.goToPage(currentPage)
|
||||||
|
}
|
||||||
|
},
|
||||||
deleteDraggableCanvas(element) {
|
deleteDraggableCanvas(element) {
|
||||||
if (element) {
|
if (element) {
|
||||||
//Check if deleted element is the last interacted
|
//Check if deleted element is the last interacted
|
||||||
@ -241,7 +304,7 @@ const DraggableUtils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const draggablesData = pagesMap[this.pageIndex];
|
const draggablesData = pagesMap[this.pageIndex];
|
||||||
if (draggablesData) {
|
if (draggablesData && Array.isArray(draggablesData)) {
|
||||||
draggablesData.forEach((draggableData) => this.boxDragContainer.appendChild(draggableData.element));
|
draggablesData.forEach((draggableData) => this.boxDragContainer.appendChild(draggableData.element));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +336,13 @@ const DraggableUtils = {
|
|||||||
|
|
||||||
//return pdfCanvas.toDataURL();
|
//return pdfCanvas.toDataURL();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async goToPage(pageIndex) {
|
||||||
|
this.storePageContents();
|
||||||
|
await this.renderPage(this.pdfDoc, pageIndex);
|
||||||
|
this.loadPageContents();
|
||||||
|
},
|
||||||
|
|
||||||
async incrementPage() {
|
async incrementPage() {
|
||||||
if (this.pageIndex < this.pdfDoc.numPages - 1) {
|
if (this.pageIndex < this.pdfDoc.numPages - 1) {
|
||||||
this.storePageContents();
|
this.storePageContents();
|
||||||
@ -297,6 +367,7 @@ const DraggableUtils = {
|
|||||||
this.storePageContents();
|
this.storePageContents();
|
||||||
|
|
||||||
const pagesMap = this.documentsMap.get(this.pdfDoc);
|
const pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||||
|
|
||||||
for (let pageIdx in pagesMap) {
|
for (let pageIdx in pagesMap) {
|
||||||
if (pageIdx.includes("offset")) {
|
if (pageIdx.includes("offset")) {
|
||||||
continue;
|
continue;
|
||||||
@ -304,7 +375,8 @@ const DraggableUtils = {
|
|||||||
console.log(typeof pageIdx);
|
console.log(typeof pageIdx);
|
||||||
|
|
||||||
const page = pdfDocModified.getPage(parseInt(pageIdx));
|
const page = pdfDocModified.getPage(parseInt(pageIdx));
|
||||||
const draggablesData = pagesMap[pageIdx];
|
let draggablesData = pagesMap[pageIdx];
|
||||||
|
|
||||||
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
|
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
|
||||||
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
|
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
|
||||||
|
|
||||||
@ -383,7 +455,6 @@ const DraggableUtils = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadPageContents();
|
this.loadPageContents();
|
||||||
return pdfDocModified;
|
return pdfDocModified;
|
||||||
},
|
},
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||||
<head>
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
|
|
||||||
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
|
|
||||||
|
|
||||||
<th:block th:each="font : ${fonts}">
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
|
||||||
|
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
|
||||||
|
|
||||||
|
<th:block th:each="font : ${fonts}">
|
||||||
<style th:inline="text">
|
<style th:inline="text">
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "[[${font.name}]]";
|
font-family: "[[${font.name}]]";
|
||||||
@ -16,372 +18,429 @@
|
|||||||
cursive;
|
cursive;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
|
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
|
||||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="page-container">
|
<div id="page-container">
|
||||||
<div id="content-wrap">
|
<div id="content-wrap">
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
<br><br>
|
<br><br>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6 bg-card">
|
<div class="col-md-6 bg-card">
|
||||||
<div class="tool-header">
|
<div class="tool-header">
|
||||||
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
|
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
|
||||||
<span class="tool-header-text" th:text="#{sign.header}"></span>
|
<span class="tool-header-text" th:text="#{sign.header}"></span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- pdf selector -->
|
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}"></div>
|
|
||||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
|
||||||
<script>
|
|
||||||
let currentPreviewSrc = null;
|
|
||||||
|
|
||||||
function toggleSignatureView() {
|
|
||||||
const gridView = document.getElementById('gridView');
|
|
||||||
const listView = document.getElementById('listView');
|
|
||||||
const gridText = document.querySelector('.grid-view-text');
|
|
||||||
const listText = document.querySelector('.list-view-text');
|
|
||||||
|
|
||||||
if (gridView.style.display !== 'none') {
|
|
||||||
gridView.style.display = 'none';
|
|
||||||
listView.style.display = 'block';
|
|
||||||
gridText.style.display = 'none';
|
|
||||||
listText.style.display = 'inline';
|
|
||||||
} else {
|
|
||||||
gridView.style.display = 'block';
|
|
||||||
listView.style.display = 'none';
|
|
||||||
gridText.style.display = 'inline';
|
|
||||||
listText.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previewSignature(element) {
|
|
||||||
const src = element.dataset.src;
|
|
||||||
currentPreviewSrc = src;
|
|
||||||
|
|
||||||
// Extract filename from the data source path
|
|
||||||
const filename = element.querySelector('.signature-list-name').textContent;
|
|
||||||
|
|
||||||
// Update preview modal
|
|
||||||
const previewImage = document.getElementById('previewImage');
|
|
||||||
const previewFileName = document.getElementById('previewFileName');
|
|
||||||
|
|
||||||
previewImage.src = src;
|
|
||||||
previewFileName.textContent = filename;
|
|
||||||
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSignatureFromPreview() {
|
|
||||||
if (currentPreviewSrc) {
|
|
||||||
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
|
|
||||||
bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let originalFileName = '';
|
|
||||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
originalFileName = file.name.replace(/\.[^/.]+$/, "");
|
|
||||||
const pdfData = await file.arrayBuffer();
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
|
||||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
|
||||||
await DraggableUtils.renderPage(pdfDoc, 0);
|
|
||||||
|
|
||||||
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
|
||||||
el.style.cssText = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
|
||||||
el.style.cssText = "display:none !important";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="tab-group show-on-file-selected">
|
|
||||||
<div class="tab-container" th:title="#{sign.upload}">
|
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
|
|
||||||
<script>
|
|
||||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
|
||||||
imageUpload.addEventListener('change', e => {
|
|
||||||
if (!e.target.files) return;
|
|
||||||
for (const imageFile of e.target.files) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.readAsDataURL(imageFile);
|
|
||||||
reader.onloadend = function (e) {
|
|
||||||
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
|
|
||||||
<canvas id="drawing-pad-canvas"></canvas>
|
|
||||||
<br>
|
|
||||||
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" th:text="#{sign.clear}"></button>
|
|
||||||
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()" th:text="#{sign.add}"></button>
|
|
||||||
<script>
|
|
||||||
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
|
|
||||||
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
|
||||||
minWidth: 1,
|
|
||||||
maxWidth: 2,
|
|
||||||
penColor: 'black',
|
|
||||||
});
|
|
||||||
|
|
||||||
function addDraggableFromPad() {
|
|
||||||
if (signaturePad.isEmpty()) return;
|
|
||||||
const startTime = Date.now();
|
|
||||||
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
|
|
||||||
console.log(Date.now() - startTime);
|
|
||||||
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCroppedCanvasDataUrl(canvas) {
|
|
||||||
let originalCtx = canvas.getContext('2d');
|
|
||||||
let originalWidth = canvas.width;
|
|
||||||
let originalHeight = canvas.height;
|
|
||||||
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
|
|
||||||
|
|
||||||
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
|
|
||||||
|
|
||||||
for (y = 0; y < originalHeight; y++) {
|
|
||||||
for (x = 0; x < originalWidth; x++) {
|
|
||||||
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
|
|
||||||
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
|
|
||||||
if (currentPixelAlphaValue > 0) {
|
|
||||||
if (minX > x) minX = x;
|
|
||||||
if (maxX < x) maxX = x;
|
|
||||||
if (minY > y) minY = y;
|
|
||||||
if (maxY < y) maxY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let croppedWidth = maxX - minX;
|
|
||||||
let croppedHeight = maxY - minY;
|
|
||||||
if (croppedWidth < 0 || croppedHeight < 0) return null;
|
|
||||||
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
|
|
||||||
|
|
||||||
let croppedCanvas = document.createElement('canvas'),
|
|
||||||
croppedCtx = croppedCanvas.getContext('2d');
|
|
||||||
|
|
||||||
croppedCanvas.width = croppedWidth;
|
|
||||||
croppedCanvas.height = croppedHeight;
|
|
||||||
croppedCtx.putImageData(cuttedImageData, 0, 0);
|
|
||||||
|
|
||||||
return croppedCanvas.toDataURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeCanvas() {
|
|
||||||
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
||||||
var additionalFactor = 10;
|
|
||||||
|
|
||||||
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
|
|
||||||
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
|
|
||||||
signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor);
|
|
||||||
|
|
||||||
signaturePad.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
new IntersectionObserver((entries, observer) => {
|
|
||||||
if (entries.some(entry => entry.intersectionRatio > 0)) {
|
|
||||||
resizeCanvas();
|
|
||||||
}
|
|
||||||
}).observe(signaturePadCanvas);
|
|
||||||
|
|
||||||
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-container" th:title="#{sign.saved}">
|
|
||||||
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
|
|
||||||
<!-- View Toggle Button -->
|
|
||||||
<div class="view-toggle mb-3">
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="toggleSignatureView()">
|
|
||||||
<span class="material-symbols-rounded grid-view-text">view_list</span>
|
|
||||||
<span class="material-symbols-rounded list-view-text" style="display: none;">grid_view</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview Modal -->
|
|
||||||
<div class="modal fade" id="signaturePreview" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title"><span id="previewFileName"></span></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body text-center">
|
|
||||||
<img id="previewImage" src="" alt="Signature Preview" style="max-width: 100%;">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
|
||||||
<button type="button" class="btn btn-primary" onclick="addSignatureFromPreview()" th:text="#{addToDoc}">Add to Document</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Grid View -->
|
|
||||||
<div id="gridView">
|
|
||||||
<!-- Personal Signatures -->
|
|
||||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
|
||||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
|
||||||
<div class="signature-grid">
|
|
||||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}" class="signature-item">
|
|
||||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}" th:data-filename="${sig.fileName}" style="max-width: 200px; cursor: pointer;" onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)"/>
|
|
||||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Shared Signatures -->
|
|
||||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
|
||||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
|
||||||
<div class="signature-grid">
|
|
||||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}" class="signature-item">
|
|
||||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}" th:data-filename="${sig.fileName}" style="max-width: 200px; cursor: pointer;" onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)"/>
|
|
||||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- List View (Initially Hidden) -->
|
|
||||||
<div id="listView" style="display: none;">
|
|
||||||
<!-- Personal Signatures -->
|
|
||||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
|
||||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
|
||||||
<div class="signature-list">
|
|
||||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}" class="signature-list-item" th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}" onclick="previewSignature(this)">
|
|
||||||
<div class="signature-list-info">
|
|
||||||
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Shared Signatures -->
|
|
||||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
|
||||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
|
||||||
<div class="signature-list">
|
|
||||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}" class="signature-list-item" th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}" onclick="previewSignature(this)">
|
|
||||||
<div class="signature-list-info">
|
|
||||||
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
|
|
||||||
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-container" th:title="#{sign.text}">
|
|
||||||
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
|
||||||
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
|
|
||||||
<label th:text="#{font}"></label>
|
|
||||||
<select class="form-control" name="font" id="font-select">
|
|
||||||
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" th:class="${font.name.toLowerCase()+'-font'}"></option>
|
|
||||||
</select>
|
|
||||||
<div class="margin-auto-parent">
|
|
||||||
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function addDraggableFromText() {
|
|
||||||
const sigText = document.getElementById('sigText').value;
|
|
||||||
const font = document.querySelector('select[name=font]').value;
|
|
||||||
const fontSize = 100;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.font = `${fontSize}px ${font}`;
|
|
||||||
const textWidth = ctx.measureText(sigText).width;
|
|
||||||
const textHeight = fontSize;
|
|
||||||
|
|
||||||
let paragraphs = sigText.split(/\r?\n/);
|
|
||||||
|
|
||||||
canvas.width = textWidth;
|
|
||||||
canvas.height = paragraphs.length * textHeight * 1.35; // for tails
|
|
||||||
ctx.font = `${fontSize}px ${font}`;
|
|
||||||
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
|
|
||||||
let y = 0;
|
|
||||||
|
|
||||||
paragraphs.forEach(paragraph => {
|
|
||||||
ctx.fillText(paragraph, 0, y);
|
|
||||||
y += fontSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataURL = canvas.toDataURL();
|
|
||||||
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- draggables box -->
|
|
||||||
<div id="box-drag-container" class="show-on-file-selected">
|
|
||||||
<canvas id="pdf-canvas"></canvas>
|
|
||||||
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
|
||||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
|
||||||
<div class="draggable-buttons-box ignore-rtl">
|
|
||||||
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
|
||||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- download button -->
|
|
||||||
<div class="margin-auto-parent">
|
|
||||||
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" th:text="#{downloadPdf}"></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById("download-pdf").addEventListener('click', async () => {
|
|
||||||
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
|
||||||
const modifiedPdfBytes = await modifiedPdf.save();
|
|
||||||
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = URL.createObjectURL(blob);
|
|
||||||
link.download = originalFileName + '_signed.pdf';
|
|
||||||
link.click();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- pdf selector -->
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
|
||||||
|
</div>
|
||||||
|
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||||
|
<script>
|
||||||
|
let currentPreviewSrc = null;
|
||||||
|
|
||||||
|
function toggleSignatureView() {
|
||||||
|
const gridView = document.getElementById('gridView');
|
||||||
|
const listView = document.getElementById('listView');
|
||||||
|
const gridText = document.querySelector('.grid-view-text');
|
||||||
|
const listText = document.querySelector('.list-view-text');
|
||||||
|
|
||||||
|
if (gridView.style.display !== 'none') {
|
||||||
|
gridView.style.display = 'none';
|
||||||
|
listView.style.display = 'block';
|
||||||
|
gridText.style.display = 'none';
|
||||||
|
listText.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
gridView.style.display = 'block';
|
||||||
|
listView.style.display = 'none';
|
||||||
|
gridText.style.display = 'inline';
|
||||||
|
listText.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewSignature(element) {
|
||||||
|
const src = element.dataset.src;
|
||||||
|
currentPreviewSrc = src;
|
||||||
|
|
||||||
|
// Extract filename from the data source path
|
||||||
|
const filename = element.querySelector('.signature-list-name').textContent;
|
||||||
|
|
||||||
|
// Update preview modal
|
||||||
|
const previewImage = document.getElementById('previewImage');
|
||||||
|
const previewFileName = document.getElementById('previewFileName');
|
||||||
|
|
||||||
|
previewImage.src = src;
|
||||||
|
previewFileName.textContent = filename;
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSignatureFromPreview() {
|
||||||
|
if (currentPreviewSrc) {
|
||||||
|
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let originalFileName = '';
|
||||||
|
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
originalFileName = file.name.replace(/\.[^/.]+$/, "");
|
||||||
|
const pdfData = await file.arrayBuffer();
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||||
|
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||||
|
|
||||||
|
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||||
|
el.style.cssText = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||||
|
el.style.cssText = "display:none !important";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<div class="tab-group show-on-file-selected">
|
||||||
|
<div class="tab-container" th:title="#{sign.upload}">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
|
||||||
|
<canvas id="drawing-pad-canvas"></canvas>
|
||||||
|
<br>
|
||||||
|
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()"
|
||||||
|
th:text="#{sign.clear}"></button>
|
||||||
|
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()"
|
||||||
|
th:text="#{sign.add}"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-container" th:title="#{sign.saved}">
|
||||||
|
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
|
||||||
|
<!-- View Toggle Button -->
|
||||||
|
<div class="view-toggle mb-3">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="toggleSignatureView()">
|
||||||
|
<span class="material-symbols-rounded grid-view-text">view_list</span>
|
||||||
|
<span class="material-symbols-rounded list-view-text" style="display: none;">grid_view</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Modal -->
|
||||||
|
<div class="modal fade" id="signaturePreview" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><span id="previewFileName"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<img id="previewImage" src="" alt="Signature Preview" style="max-width: 100%;">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
|
||||||
|
th:text="#{close}"></button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="addSignatureFromPreview()"
|
||||||
|
th:text="#{addToDoc}">Add to Document</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid View -->
|
||||||
|
<div id="gridView">
|
||||||
|
<!-- Personal Signatures -->
|
||||||
|
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||||
|
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||||
|
<div class="signature-grid">
|
||||||
|
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}" class="signature-item">
|
||||||
|
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||||
|
th:data-filename="${sig.fileName}" style="max-width: 200px; cursor: pointer;"
|
||||||
|
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)" />
|
||||||
|
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shared Signatures -->
|
||||||
|
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||||
|
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||||
|
<div class="signature-grid">
|
||||||
|
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}" class="signature-item">
|
||||||
|
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||||
|
th:data-filename="${sig.fileName}" style="max-width: 200px; cursor: pointer;"
|
||||||
|
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)" />
|
||||||
|
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List View (Initially Hidden) -->
|
||||||
|
<div id="listView" style="display: none;">
|
||||||
|
<!-- Personal Signatures -->
|
||||||
|
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||||
|
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||||
|
<div class="signature-list">
|
||||||
|
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
|
||||||
|
class="signature-list-item" th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||||
|
onclick="previewSignature(this)">
|
||||||
|
<div class="signature-list-info">
|
||||||
|
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shared Signatures -->
|
||||||
|
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||||
|
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||||
|
<div class="signature-list">
|
||||||
|
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
|
||||||
|
class="signature-list-item" th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||||
|
onclick="previewSignature(this)">
|
||||||
|
<div class="signature-list-info">
|
||||||
|
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
|
||||||
|
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-container" th:title="#{sign.text}">
|
||||||
|
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
||||||
|
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
|
||||||
|
<label th:text="#{font}"></label>
|
||||||
|
<select class="form-control" name="font" id="font-select">
|
||||||
|
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}"
|
||||||
|
th:class="${font.name.toLowerCase()+'-font'}"></option>
|
||||||
|
</select>
|
||||||
|
<div class="margin-auto-parent">
|
||||||
|
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center"
|
||||||
|
onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||||
|
imageUpload.addEventListener('change', e => {
|
||||||
|
if (!e.target.files) return;
|
||||||
|
for (const imageFile of e.target.files) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.readAsDataURL(imageFile);
|
||||||
|
reader.onloadend = function (e) {
|
||||||
|
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
|
||||||
|
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
||||||
|
minWidth: 1,
|
||||||
|
maxWidth: 2,
|
||||||
|
penColor: 'black',
|
||||||
|
});
|
||||||
|
|
||||||
|
function addDraggableFromPad() {
|
||||||
|
if (signaturePad.isEmpty()) return;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
|
||||||
|
console.log(Date.now() - startTime);
|
||||||
|
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCroppedCanvasDataUrl(canvas) {
|
||||||
|
let originalCtx = canvas.getContext('2d');
|
||||||
|
let originalWidth = canvas.width;
|
||||||
|
let originalHeight = canvas.height;
|
||||||
|
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
|
||||||
|
|
||||||
|
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
|
||||||
|
|
||||||
|
for (y = 0; y < originalHeight; y++) {
|
||||||
|
for (x = 0; x < originalWidth; x++) {
|
||||||
|
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
|
||||||
|
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
|
||||||
|
if (currentPixelAlphaValue > 0) {
|
||||||
|
if (minX > x) minX = x;
|
||||||
|
if (maxX < x) maxX = x;
|
||||||
|
if (minY > y) minY = y;
|
||||||
|
if (maxY < y) maxY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let croppedWidth = maxX - minX;
|
||||||
|
let croppedHeight = maxY - minY;
|
||||||
|
if (croppedWidth < 0 || croppedHeight < 0) return null;
|
||||||
|
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
|
||||||
|
|
||||||
|
let croppedCanvas = document.createElement('canvas'),
|
||||||
|
croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCanvas.width = croppedWidth;
|
||||||
|
croppedCanvas.height = croppedHeight;
|
||||||
|
croppedCtx.putImageData(cuttedImageData, 0, 0);
|
||||||
|
|
||||||
|
return croppedCanvas.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||||
|
var additionalFactor = 10;
|
||||||
|
|
||||||
|
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
|
||||||
|
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
|
||||||
|
signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor);
|
||||||
|
|
||||||
|
signaturePad.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
new IntersectionObserver((entries, observer) => {
|
||||||
|
if (entries.some(entry => entry.intersectionRatio > 0)) {
|
||||||
|
resizeCanvas();
|
||||||
|
}
|
||||||
|
}).observe(signaturePadCanvas);
|
||||||
|
|
||||||
|
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
function addDraggableFromText() {
|
||||||
|
const sigText = document.getElementById('sigText').value;
|
||||||
|
const font = document.querySelector('select[name=font]').value;
|
||||||
|
const fontSize = 100;
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.font = `${fontSize}px ${font}`;
|
||||||
|
const textWidth = ctx.measureText(sigText).width;
|
||||||
|
const textHeight = fontSize;
|
||||||
|
|
||||||
|
let paragraphs = sigText.split(/\r?\n/);
|
||||||
|
|
||||||
|
canvas.width = textWidth;
|
||||||
|
canvas.height = paragraphs.length * textHeight * 1.35; // for tails
|
||||||
|
ctx.font = `${fontSize}px ${font}`;
|
||||||
|
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
let y = 0;
|
||||||
|
|
||||||
|
paragraphs.forEach(paragraph => {
|
||||||
|
ctx.fillText(paragraph, 0, y);
|
||||||
|
y += fontSize;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataURL = canvas.toDataURL();
|
||||||
|
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- draggables box -->
|
||||||
|
<div id="box-drag-container" class="show-on-file-selected">
|
||||||
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
||||||
|
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||||
|
<div class="draggable-buttons-box ignore-rtl">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||||
|
viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||||
|
<path
|
||||||
|
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||||
|
<span class="material-symbols-rounded">
|
||||||
|
content_copy
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(false)" style="margin-left:auto">
|
||||||
|
<span class="material-symbols-rounded">
|
||||||
|
keyboard_double_arrow_left
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" id="incrementPage"
|
||||||
|
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" id="decrementPage"
|
||||||
|
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(true)">
|
||||||
|
<span class="material-symbols-rounded">
|
||||||
|
keyboard_double_arrow_right
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- download button -->
|
||||||
|
<div class="margin-auto-parent">
|
||||||
|
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center"
|
||||||
|
th:text="#{downloadPdf}"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function goToFirstOrLastPage(page) {
|
||||||
|
if (page) {
|
||||||
|
const lastPage = DraggableUtils.pdfDoc.numPages
|
||||||
|
await DraggableUtils.goToPage(lastPage - 1)
|
||||||
|
} else {
|
||||||
|
await DraggableUtils.goToPage(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("download-pdf").addEventListener('click', async () => {
|
||||||
|
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
||||||
|
const modifiedPdfBytes = await modifiedPdf.save();
|
||||||
|
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = originalFileName + '_signed.pdf';
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Link the draggable.js file -->
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
<script th:src="@{'/js/draggable.js'}"></script>
|
</div>
|
||||||
</body>
|
<!-- Link the draggable.js file -->
|
||||||
|
<script th:src="@{'/js/draggable.js'}"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user