1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-11-19 05:40:10 +01:00

Revamped Signing page to allow multiple signatures and images

This commit is contained in:
Saud Fatayerji 2023-05-07 23:39:34 +03:00
parent 07e81a117b
commit b86afd7fc9
6 changed files with 479 additions and 350 deletions

View File

@ -16,11 +16,14 @@
html[lang-direction=ltr] * { html[lang-direction=ltr] * {
direction: ltr; direction: ltr;
} }
html[lang-direction=rtl] * { html[lang-direction=rtl] * {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
.ignore-rtl {
direction: ltr !important;
text-align: left !important;
}
.align-top { .align-top {
position: absolute; position: absolute;
@ -47,3 +50,11 @@ html[lang-direction=rtl] * {
border-top-left-radius: 0.25rem !important; border-top-left-radius: 0.25rem !important;
border-bottom-left-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important;
} }
.margin-auto-parent {
width: 100%;
display: flex;
}
.margin-center {
margin: 0 auto;
}

View File

@ -0,0 +1,31 @@
.tab-group {
}
.tab-container {
display: none;
}
.tab-container.active {
display: block;
border: 1px solid #dee2e6;
padding: 15px;
}
/*
.tab-buttons {
border-bottom: 1px solid #dee2e6;
}
*/
.tab-buttons > button {
margin-bottom: -1px;
background: 0 0;
border: 1px solid transparent;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.tab-buttons > button.active {
color: #495057;
background-color: #fff;
border-color: #dee2e6 #dee2e6 #fff;
}

View File

@ -0,0 +1,211 @@
const DraggableUtils = {
boxDragContainer: document.getElementById('box-drag-container'),
pdfCanvas: document.getElementById('pdf-canvas'),
nextId: 0,
pdfDoc: null,
pageIndex: 0,
init() {
interact('.draggable-canvas')
.draggable({
listeners: {
move: (event) => {
const target = event.target;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
this.onInteraction(target);
},
},
})
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move: (event) => {
var target = event.target
var x = (parseFloat(target.getAttribute('data-x')) || 0)
var y = (parseFloat(target.getAttribute('data-y')) || 0)
// update the element's style
target.style.width = event.rect.width + 'px'
target.style.height = event.rect.height + 'px'
// translate when resizing from top or left edges
x += event.deltaRect.left
y += event.deltaRect.top
target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
target.setAttribute('data-x', x)
target.setAttribute('data-y', y)
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
this.onInteraction(target);
},
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: 50, height: 50 },
}),
],
inertia: true,
});
},
onInteraction(target) {
this.boxDragContainer.appendChild(target);
},
createDraggableCanvas() {
const createdCanvas = document.createElement('canvas');
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
createdCanvas.classList.add("draggable-canvas");
const x = 0;
const y = 0;
createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
createdCanvas.setAttribute('data-x', x);
createdCanvas.setAttribute('data-y', y);
createdCanvas.onclick = e => this.onInteraction(e.target);
this.boxDragContainer.appendChild(createdCanvas);
return createdCanvas;
},
createDraggableCanvasFromUrl(dataUrl) {
return new Promise((resolve) => {
var myImage = new Image();
myImage.src = dataUrl;
myImage.onload = () => {
var createdCanvas = this.createDraggableCanvas();
createdCanvas.width = myImage.width;
createdCanvas.height = myImage.height;
const imgAspect = myImage.width / myImage.height;
const pdfAspect = this.boxDragContainer.offsetWidth / this.boxDragContainer.offsetHeight;
var scaleMultiplier;
if (imgAspect > pdfAspect) {
scaleMultiplier = this.boxDragContainer.offsetWidth / myImage.width;
} else {
scaleMultiplier = this.boxDragContainer.offsetHeight / myImage.height;
}
var newWidth = createdCanvas.width;
var newHeight = createdCanvas.height;
if (scaleMultiplier < 1) {
newWidth = newWidth * scaleMultiplier;
newHeight = newHeight * scaleMultiplier;
}
createdCanvas.style.width = newWidth+"px";
createdCanvas.style.height = newHeight+"px";
var myContext = createdCanvas.getContext("2d");
myContext.drawImage(myImage,0,0);
resolve(createdCanvas);
}
})
},
deleteDraggableCanvas(element) {
element.remove();
},
deleteDraggableCanvasById(id) {
this.deleteDraggableCanvas(document.getElementById(id));
},
getLastInteracted() {
return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
},
async renderPage(pdfDocument, pageIdx) {
this.pdfDoc = pdfDocument ? pdfDocument : this.pdfDoc;
this.pageIndex = pageIdx;
const page = await this.pdfDoc.getPage(this.pageIndex+1);
// set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) {
this.pdfCanvas.width = page.view[3];
this.pdfCanvas.height = page.view[2];
} else {
this.pdfCanvas.width = page.view[2];
this.pdfCanvas.height = page.view[3];
}
// render the page onto the canvas
var renderContext = {
canvasContext: this.pdfCanvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 })
};
await page.render(renderContext).promise;
//return pdfCanvas.toDataURL();
},
async incrementPage() {
if (this.pageIndex < this.pdfDoc.numPages-1) {
return await this.renderPage(this.pdfDoc, this.pageIndex+1)
}
},
async decrementPage() {
if (this.pageIndex > 0) {
return await this.renderPage(this.pdfDoc, this.pageIndex-1)
}
},
parseTransform(element) {
const tansform = element.style.transform.replace(/[^.,-\d]/g, '');
const transformComponents = tansform.split(",");
return {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
width: element.offsetWidth,
height: element.offsetHeight,
}
},
async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes);
const draggables = this.boxDragContainer.querySelectorAll(".draggable-canvas");
for (const draggable of draggables) {
// embed the draggable canvas
const dataURL = draggable.toDataURL();
const response = await fetch(dataURL);
const draggableImgBytes = await response.arrayBuffer();
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
const page = pdfDocModified.getPage(this.pageIndex);
const draggablePositionPixels = this.parseTransform(draggable);
const draggablePositionRelative = {
x: draggablePositionPixels.x / this.pdfCanvas.offsetWidth,
y: draggablePositionPixels.y / this.pdfCanvas.offsetHeight,
width: draggablePositionPixels.width / this.pdfCanvas.offsetWidth,
height: draggablePositionPixels.height / this.pdfCanvas.offsetHeight,
}
const draggablePositionPdf = {
x: draggablePositionRelative.x * page.getWidth(),
y: draggablePositionRelative.y * page.getHeight(),
width: draggablePositionRelative.width * page.getWidth(),
height: draggablePositionRelative.height * page.getHeight(),
}
page.drawImage(pdfImageObject, {
x: draggablePositionPdf.x,
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
});
}
return pdfDocModified;
},
}
document.addEventListener("DOMContentLoaded", () => {
DraggableUtils.init();
});

View File

@ -0,0 +1,39 @@
TabContainer = {
initTabGroups() {
const groups = document.querySelectorAll(".tab-group");
const unloadedGroups = [...groups].filter(g => !g.initialised);
unloadedGroups.forEach(group => {
const containers = group.querySelectorAll(".tab-container");
const tabTitles = [...containers].map(c => c.getAttribute("title"));
const tabList = document.createElement("div");
tabList.classList.add("tab-buttons");
tabTitles.forEach(title => {
const tabButton = document.createElement("button");
tabButton.innerHTML = title;
tabButton.onclick = e => {
this.setActiveTab(e.target);
}
tabList.appendChild(tabButton);
});
group.prepend(tabList);
this.setActiveTab(tabList.firstChild);
group.initialised = true;
});
},
setActiveTab(tabButton) {
const group = tabButton.closest(".tab-group")
group.querySelectorAll(".active").forEach(el => el.classList.remove("active"));
tabButton.classList.add("active");
group.querySelector(`[title="${tabButton.innerHTML}"]`).classList.add("active");
},
}
document.addEventListener("DOMContentLoaded", () => {
TabContainer.initTabGroups();
})

View File

@ -29,6 +29,10 @@
<link rel="stylesheet" href="css/general.css"> <link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles"> <link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
<link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true"> <link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true">
<link rel="stylesheet" href="css/tab-container.css">
<script src="js/tab-container.js"></script>
<script> <script>
var toggleCount = 0; var toggleCount = 0;
var lastToggleTime = Date.now(); var lastToggleTime = Date.now();
@ -478,7 +482,6 @@ document.addEventListener("DOMContentLoaded", function () {
</div> </div>
<div class="selected-files"></div> <div class="selected-files"></div>
</div> </div>
<br>
<div id="progressBarContainer" style="display: none; position: relative;"> <div id="progressBarContainer" style="display: none; position: relative;">
<div class="progress" style="height: 1rem;"> <div class="progress" style="height: 1rem;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"> <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">

View File

@ -13,56 +13,6 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Estonia&family=Tangerine&family=Windsong&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Estonia&family=Tangerine&family=Windsong&display=swap" rel="stylesheet">
<style>
#pdf-container {
position: relative;
}
#pdf-canvas {
border: 1px solid black;
margin-top: 10px;
}
#signature-canvas {
border: 1px solid red;
position: absolute;
touch-action: none;
top: 10px; /* Make sure this value matches the margin-top of #pdf-canvas */
left: 0;
}
#signature-pad-container {
border: 1px solid black;
display: block;
text-align: center;
padding: 15px;
}
#signature-pad-canvas {
background: rgba(125,125,125,0.2);
}
#pdf-canvas {
width: 100%;
}
#font-select option {
font-size: 30px; /* Set the font size as desired */
}
#font-select option[value="Estonia"] {
font-family: 'Estonia', sans-serif;
}
#font-select option[value="Tangerine"] {
font-family: 'Tangerine', cursive;
}
#font-select option[value="Windsong"] {
font-family: 'Windsong', cursive;
}
</style>
</head> </head>
<body> <body>
@ -75,28 +25,78 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{sign.header}"></h2> <h2 th:text="#{sign.header}"></h2>
<!-- pdf selector -->
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
<script>
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const pdfData = await file.arrayBuffer();
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
<div class = "btn-group"> document.querySelectorAll(".show-on-file-selected").forEach(el => {
<input type="radio" class="btn-check" name="signature-type" id="draw-signature" autocomplete="off" checked> el.style.cssText = '';
<label class="btn btn-outline-secondary" for="draw-signature">Draw</label> })
<input type="radio" class="btn-check" name="signature-type" id="import-image" autocomplete="off"> }
<label class="btn btn-outline-secondary" for="import-image">Import</label> });
<input type="radio" class="btn-check" name="signature-type" id="type-signature" autocomplete="off"> document.addEventListener("DOMContentLoaded", () => {
<label class="btn btn-outline-secondary" for="type-signature">Type</label> 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" title="Upload Image">
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=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>
<div class="tab-container drawing-pad-container" title="Draw Signature">
<div th:replace="~{fragments/common :: fileSelector(name='signature-upload', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div> <canvas id="drawing-pad-canvas"></canvas>
<!-- Signature Pad -->
<div id="signature-pad-container">
<canvas id="signature-pad-canvas"></canvas>
<br> <br>
<button id="clear-signature" class="btn btn-outline-danger mt-2">Clear</button> <button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()">Clear</button>
<button id="save-signature" class="btn btn-outline-success mt-2">Save</button> <button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()">Add</button>
</div> <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 dataURL = signaturePad.toDataURL();
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
}
</script>
<style>
.drawing-pad-container {
text-align: center;
}
<div id="signature-type-container"> #drawing-pad-canvas {
<label class="form-check-label" for="sigText">Signature</label> background: rgba(125,125,125,0.2);
}
</style>
</div>
<div class="tab-container" title="Text Input">
<label class="form-check-label" for="sigText">Text</label>
<input type="text" class="form-control" id="sigText" name="sigText"> <input type="text" class="form-control" id="sigText" name="sigText">
<label>Font:</label> <label>Font:</label>
<select class="form-control" name="font" id="font-select"> <select class="form-control" name="font" id="font-select">
@ -104,92 +104,14 @@
<option value="Tangerine" class="tangerine-font">Tangerine</option> <option value="Tangerine" class="tangerine-font">Tangerine</option>
<option value="Windsong" class="windsong-font">Windsong</option> <option value="Windsong" class="windsong-font">Windsong</option>
</select> </select>
<button id="save-text-signature" class="btn btn-outline-success mt-2">Save</button> <div class="margin-auto-parent">
</div> <button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()">Add</button>
<div id="pdf-container">
<canvas id="pdf-canvas"></canvas>
<canvas id="signature-canvas" hidden style="position: absolute;" data-scale="1"></canvas>
</div>
<button id="download-pdf" class="btn btn-primary mb-2">Download PDF</button>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { function addDraggableFromText() {
const fontSelect = document.getElementById('font-select');
fontSelect.addEventListener('change', (event) => {
const selectedFont = event.target.value;
fontSelect.style.fontFamily = selectedFont;
});
fontSelect.style.fontFamily = fontSelect.value;
const pdfUpload = document.querySelector('input[name=pdf-upload]');
const signatureUpload = document.querySelector('input[name=signature-upload]');
const pdfCanvas = document.getElementById('pdf-canvas');
const signatureCanvas = document.getElementById('signature-canvas');
const downloadPdfBtn = document.getElementById('download-pdf');
const signaturePadContainer = document.getElementById('signature-pad-container')
const signaturePadCanvas = document.getElementById('signature-pad-canvas');
const clearSignatureBtn = document.getElementById('clear-signature');
const saveSignatureBtn = document.getElementById('save-signature');
const signatureTypeContainer = document.getElementById('signature-type-container');
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = "none"
signatureTypeContainer.style.display = 'none';
const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1,
maxWidth: 2,
penColor: 'black',
});
const pdfCtx = pdfCanvas.getContext('2d');
let pdfDoc = null;
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js';
pdfUpload.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const pdfData = await file.arrayBuffer();
pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
renderPage(1);
}
});
clearSignatureBtn.addEventListener('click', () => {
signaturePad.clear();
});
$("input[name=signature-type]").change(function() {
const drawSignatureInput = document.getElementById('draw-signature');
const importImageInput = document.getElementById('import-image');
const typeSignatureInput = document.getElementById('type-signature');
signaturePadContainer.style.display = drawSignatureInput.checked ? 'block' : 'none';
signatureTypeContainer.style.display = typeSignatureInput.checked ? 'block' : 'none';
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = importImageInput.checked ? 'block' : 'none';
if (drawSignatureInput.checked) {
populateSignatureFromPad();
} else if (importImageInput.checked) {
populateSignatureFromFileUpload();
}
});
const saveTextSignatureBtn = document.getElementById('save-text-signature');
saveTextSignatureBtn.addEventListener('click', () => {
if (!document.getElementById('type-signature').checked) return;
const sigText = document.getElementById('sigText').value; const sigText = document.getElementById('sigText').value;
const font = document.querySelector('select[name=font]').value; const font = document.querySelector('select[name=font]').value;
const fontSize = 50; const fontSize = 100;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -203,188 +125,100 @@
ctx.fillText(sigText, 0, fontSize); ctx.fillText(sigText, 0, fontSize);
const dataURL = canvas.toDataURL(); const dataURL = canvas.toDataURL();
populateSignature(dataURL); DraggableUtils.createDraggableCanvasFromUrl(dataURL);
});
function populateSignature(imgUrl) {
const img = new Image();
img.onload = () => {
const ctx = signatureCanvas.getContext('2d');
ctx.clearRect(0, 0, signatureCanvas.width, signatureCanvas.height);
signatureCanvas.width = img.width;
signatureCanvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
signatureCanvas.hidden = false;
const x = 0;
const y = 0;
signatureCanvas.style.transform = `translate(${x}px, ${y}px)`;
signatureCanvas.setAttribute('data-x', x);
signatureCanvas.setAttribute('data-y', y);
// calcualte the max size
const containerWidth = parseInt(getComputedStyle(pdfCanvas).width.replace('px',''));
const containerHeight = parseInt(getComputedStyle(pdfCanvas).height.replace('px',''));
const containerAspectRatio = containerWidth / containerHeight;
const imgAspectRatio = img.width / img.height;
if (imgAspectRatio > containerAspectRatio) {
const width = Math.min(img.width, containerWidth);
signatureCanvas.style.width = width+'px';
signatureCanvas.style.height = (width/imgAspectRatio)+'px';
} else {
const height = Math.min(img.height, containerHeight);
signatureCanvas.style.width = (height*imgAspectRatio)+'px';
signatureCanvas.style.height = height+'px';
} }
}; </script>
img.src = imgUrl; <style>
#font-select option {
font-size: 30px;
} }
function populateSignatureFromFileUpload() { #font-select option[value="Estonia"] {
const file = signatureUpload.files[0]; font-family: 'Estonia', sans-serif;
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => populateSignature(e.target.result);
reader.readAsDataURL(file);
} }
function populateSignatureFromPad() { #font-select option[value="Tangerine"] {
if (!document.getElementById('draw-signature').checked) return; font-family: 'Tangerine', cursive;
if (signaturePad.isEmpty()) return;
const dataURL = signaturePad.toDataURL();
populateSignature(dataURL);
} }
signatureUpload.addEventListener('change', populateSignatureFromFileUpload); #font-select option[value="Windsong"] {
saveSignatureBtn.addEventListener('click', populateSignatureFromPad); font-family: 'Windsong', cursive;
function renderPage(pageNum) {
pdfDoc.getPage(pageNum).then((page) => {
const viewport = page.getViewport({ scale: 1 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
const renderCtx = {
canvasContext: pdfCtx,
viewport: viewport,
};
page.render(renderCtx);
});
} }
</style>
</div>
</div>
function parseTransform(element) { <!-- draggables box -->
const tansform = element.style.transform.replace(/[^.,-\d]/g, ''); <div id="box-drag-container" class="show-on-file-selected">
const transformComponents = tansform.split(","); <canvas id="pdf-canvas"></canvas>
return { <script src="/js/draggable-utils.js"></script>
x: parseFloat(transformComponents[0]), <div class="draggable-buttons-box ignore-rtl">
y: parseFloat(transformComponents[1]), <button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
width: element.offsetWidth, <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
height: element.offsetHeight, <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('lang-direction')==='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('lang-direction')==='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>
<style>
#box-drag-container {
position: relative;
} }
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
margin: 20px 0;
width: 100%;
} }
interact('#signature-canvas') .draggable-buttons-box {
.draggable({ position: absolute;
listeners: { top: 20px;
move: (event) => { padding: 10px;
const target = event.target; width: 100%;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx; display: flex;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy; gap: 5px;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
},
},
})
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move: (event) => {
var target = event.target
var x = (parseFloat(target.getAttribute('data-x')) || 0)
var y = (parseFloat(target.getAttribute('data-y')) || 0)
// update the element's style
target.style.width = event.rect.width + 'px'
target.style.height = event.rect.height + 'px'
// translate when resizing from top or left edges
x += event.deltaRect.left
y += event.deltaRect.top
target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
target.setAttribute('data-x', x)
target.setAttribute('data-y', y)
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
},
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: 50, height: 50 },
}),
],
inertia: true,
});
async function getSignatureImage() {
const dataURL = signatureCanvas.toDataURL();
return dataURLToArrayBuffer(dataURL);
} }
.draggable-buttons-box > button {
downloadPdfBtn.addEventListener('click', async () => { z-index: 10;
if (pdfDoc) { background: rgba(13, 110, 253, 0.1);
const pdfBytes = await pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes);
if (signatureCanvas) {
const signatureBytes = await getSignatureImage();
const signatureImageObject = await pdfDocModified.embedPng(signatureBytes);
const pageIndex = 0; // Choose the page index where the signature should be added (0 is the first page)
const page = pdfDocModified.getPages()[pageIndex];
const signatureCanvasPositionPixels = parseTransform(signatureCanvas);
const signatureCanvasPositionRelative = {
x: signatureCanvasPositionPixels.x / pdfCanvas.offsetWidth,
y: signatureCanvasPositionPixels.y / pdfCanvas.offsetHeight,
width: signatureCanvasPositionPixels.width / pdfCanvas.offsetWidth,
height: signatureCanvasPositionPixels.height / pdfCanvas.offsetHeight,
} }
const signaturePositionPdf = { .draggable-canvas {
x: signatureCanvasPositionRelative.x * page.getWidth(), border: 1px solid red;
y: signatureCanvasPositionRelative.y * page.getHeight(), position: absolute;
width: signatureCanvasPositionRelative.width * page.getWidth(), touch-action: none;
height: signatureCanvasPositionRelative.height * page.getHeight(), user-select: none;
top: 0px;
left: 0;
} }
</style>
</div>
page.drawImage(signatureImageObject, { <!-- download button -->
x: signaturePositionPdf.x, <div class="margin-auto-parent">
y: page.getHeight() - signaturePositionPdf.y - signaturePositionPdf.height, <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
width: signaturePositionPdf.width, </div>
height: signaturePositionPdf.height, <script>
}); document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
} const modifiedPdfBytes = await modifiedPdf.save();
const modifiedPdfBytes = await pdfDocModified.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' }); const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a'); const link = document.createElement('a');
link.href = URL.createObjectURL(blob); link.href = URL.createObjectURL(blob);
link.download = 'signed-document.pdf'; link.download = 'signed-document.pdf';
link.click(); link.click();
}
});
async function dataURLToArrayBuffer(dataURL) {
const response = await fetch(dataURL);
return response.arrayBuffer();
}
}); });
</script> </script>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body> </body>
</html> </html>