2023-05-02 23:59:16 +02:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
|
|
|
|
|
|
|
<head>
|
|
|
|
<th:block th:insert="~{fragments/common :: head(title=#{extractImages.title})}"></th:block>
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<title>PDF Signature App</title>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.min.js"></script>
|
|
|
|
<script src="https://unpkg.com/pdf-lib@1.18.0/dist/umd/pdf-lib.min.js"></script>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.5/dist/signature_pad.umd.min.js"></script>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/interact.js/1.10.11/interact.min.js"></script>
|
|
|
|
<style>
|
|
|
|
#pdf-container {
|
2023-05-03 23:07:51 +02:00
|
|
|
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;
|
|
|
|
}
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
#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%;
|
|
|
|
}
|
2023-05-02 23:59:16 +02:00
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
2023-05-03 23:07:51 +02:00
|
|
|
<th:block th:insert="~{fragments/common.html :: game}"></th:block>
|
2023-05-02 23:59:16 +02:00
|
|
|
<div id="page-container">
|
|
|
|
<div id="content-wrap">
|
|
|
|
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
|
|
|
<br> <br>
|
|
|
|
<div class="container">
|
|
|
|
<div class="row justify-content-center">
|
|
|
|
<div class="col-md-6">
|
|
|
|
<h2 th:text="#{extractImages.header}"></h2>
|
2023-05-03 23:07:51 +02:00
|
|
|
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
|
|
|
|
|
|
|
|
<div class = "btn-group">
|
|
|
|
<input type="radio" class="btn-check" name="signature-type" id="draw-signature" autocomplete="off" checked>
|
|
|
|
<label class="btn btn-outline-secondary" for="draw-signature">Draw signature</label>
|
2023-05-05 00:19:05 +02:00
|
|
|
<input type="radio" class="btn-check" name="signature-type" id="generate-signature" autocomplete="off">
|
|
|
|
<label class="btn btn-outline-secondary" for="generate-signature">Generate Signature</label>
|
2023-05-03 23:07:51 +02:00
|
|
|
<input type="radio" class="btn-check" name="signature-type" id="import-image" autocomplete="off">
|
|
|
|
<label class="btn btn-outline-secondary" for="import-image">Import image</label>
|
2023-05-05 00:19:05 +02:00
|
|
|
|
2023-05-02 23:59:16 +02:00
|
|
|
</div>
|
2023-05-03 23:07:51 +02:00
|
|
|
|
|
|
|
<div th:replace="~{fragments/common :: fileSelector(name='signature-upload', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div>
|
2023-05-02 23:59:16 +02:00
|
|
|
<!-- Signature Pad -->
|
2023-05-03 23:07:51 +02:00
|
|
|
<div id="signature-pad-container">
|
2023-05-02 23:59:16 +02:00
|
|
|
<canvas id="signature-pad-canvas"></canvas>
|
2023-05-03 23:07:51 +02:00
|
|
|
<br>
|
2023-05-02 23:59:16 +02:00
|
|
|
<button id="clear-signature" class="btn btn-outline-danger mt-2">Clear</button>
|
|
|
|
<button id="save-signature" class="btn btn-outline-success mt-2">Save</button>
|
|
|
|
</div>
|
2023-05-05 00:19:05 +02:00
|
|
|
<div id="signature-text-input-container" style="display: none;">
|
|
|
|
<input type="text" id="signature-text-input" class="form-control mt-2" placeholder="Type your signature here...">
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
<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>
|
2023-05-02 23:59:16 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
|
|
|
</div>
|
|
|
|
<script>
|
2023-05-03 23:07:51 +02:00
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const pdfUpload = document.querySelector('input[name=pdf-upload]');
|
|
|
|
const signatureUpload = document.querySelector('input[name=signature-upload]');
|
2023-05-05 00:19:05 +02:00
|
|
|
const signatureTextInput = document.querySelector('input[name=signatureTextInput]');
|
2023-05-03 23:07:51 +02:00
|
|
|
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');
|
|
|
|
|
|
|
|
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = "none"
|
|
|
|
|
|
|
|
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
|
|
|
minWidth: 1,
|
|
|
|
maxWidth: 2,
|
|
|
|
penColor: 'black',
|
|
|
|
});
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
|
|
|
|
const pdfCtx = pdfCanvas.getContext('2d');
|
|
|
|
let pdfDoc = null;
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js';
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
clearSignatureBtn.addEventListener('click', () => {
|
|
|
|
signaturePad.clear();
|
|
|
|
});
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-05 00:19:05 +02:00
|
|
|
$("input[name=signature-type]").change(function () {
|
|
|
|
const drawSignatureInput = document.getElementById("draw-signature");
|
|
|
|
const generateSignatureInput = document.getElementById("generate-signature");
|
|
|
|
const importImageInputContainer = document.querySelector("input[name=signature-upload]").closest(".custom-file-chooser");
|
|
|
|
signaturePadContainer.style.display = drawSignatureInput.checked ? "block" : "none";
|
|
|
|
importImageInputContainer.style.display = drawSignatureInput.checked ? "none" : (generateSignatureInput.checked ? "none" : "block");
|
|
|
|
document.getElementById("signature-text-input-container").style.display = generateSignatureInput.checked ? "block" : "none";
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
if (drawSignatureInput.checked) {
|
|
|
|
populateSignatureFromPad();
|
2023-05-05 00:19:05 +02:00
|
|
|
} else if (generateSignatureInput.checked) {
|
|
|
|
populateSignatureFromText();
|
2023-05-03 23:07:51 +02:00
|
|
|
} else {
|
|
|
|
populateSignatureFromFileUpload();
|
|
|
|
}
|
|
|
|
});
|
2023-05-05 00:19:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
function populateSignatureFromText() {
|
|
|
|
const signatureText = document.getElementById("signature-text-input").value;
|
|
|
|
if (!signatureText) return;
|
|
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
ctx.font = "32px Arial";
|
|
|
|
const textMetrics = ctx.measureText(signatureText);
|
|
|
|
|
|
|
|
canvas.width = textMetrics.width;
|
|
|
|
canvas.height = parseInt(ctx.font, 10);
|
|
|
|
|
|
|
|
ctx.fillStyle = "black";
|
|
|
|
ctx.font = "32px Arial";
|
|
|
|
ctx.fillText(signatureText, 0, canvas.height * 0.8);
|
|
|
|
|
|
|
|
const dataURL = canvas.toDataURL();
|
|
|
|
populateSignature(dataURL);
|
|
|
|
}
|
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
|
|
|
|
function populateSignature(imgUrl) {
|
2023-05-02 23:59:16 +02:00
|
|
|
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;
|
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
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';
|
|
|
|
}
|
2023-05-02 23:59:16 +02:00
|
|
|
};
|
2023-05-03 23:07:51 +02:00
|
|
|
img.src = imgUrl;
|
|
|
|
}
|
|
|
|
function populateSignatureFromFileUpload() {
|
|
|
|
const file = signatureUpload.files[0];
|
|
|
|
if (!file) return;
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (e) => populateSignature(e.target.result);
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
}
|
|
|
|
function populateSignatureFromPad() {
|
|
|
|
if (signaturePad.isEmpty()) return;
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
const dataURL = signaturePad.toDataURL();
|
|
|
|
populateSignature(dataURL);
|
|
|
|
}
|
|
|
|
signatureUpload.addEventListener('change', populateSignatureFromFileUpload);
|
2023-05-05 00:19:05 +02:00
|
|
|
signatureTextInput.addEventListener('change', populateSignatureFromText);
|
2023-05-03 23:07:51 +02:00
|
|
|
saveSignatureBtn.addEventListener('click', populateSignatureFromPad);
|
2023-05-02 23:59:16 +02:00
|
|
|
|
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
function renderPage(pageNum) {
|
|
|
|
pdfDoc.getPage(pageNum).then((page) => {
|
|
|
|
const viewport = page.getViewport({ scale: 1 });
|
|
|
|
pdfCanvas.width = viewport.width;
|
|
|
|
pdfCanvas.height = viewport.height;
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
const renderCtx = {
|
|
|
|
canvasContext: pdfCtx,
|
|
|
|
viewport: viewport,
|
|
|
|
};
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
page.render(renderCtx);
|
|
|
|
});
|
|
|
|
}
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
interact('#signature-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);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.resizable({
|
|
|
|
edges: { left: true, right: true, bottom: true, top: true },
|
|
|
|
listeners: {
|
|
|
|
move: (event) => {
|
|
|
|
const target = event.target;
|
|
|
|
const x = (parseFloat(target.getAttribute('data-x')) || 0);
|
|
|
|
const y = (parseFloat(target.getAttribute('data-y')) || 0);
|
|
|
|
|
|
|
|
const newWidth = event.rect.width;
|
|
|
|
const newHeight = event.rect.height;
|
|
|
|
const scale = newWidth / target.width;
|
|
|
|
|
|
|
|
target.style.width = newWidth + 'px';
|
|
|
|
target.style.height = newHeight + 'px';
|
|
|
|
target.setAttribute('data-scale', scale);
|
|
|
|
|
|
|
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
modifiers: [
|
|
|
|
interact.modifiers.restrictSize({
|
|
|
|
min: { width: 50, height: 50 },
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
inertia: true,
|
|
|
|
});
|
2023-05-02 23:59:16 +02:00
|
|
|
|
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
async function getSignatureImage() {
|
|
|
|
const dataURL = signatureCanvas.toDataURL();
|
|
|
|
return dataURLToArrayBuffer(dataURL);
|
|
|
|
}
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
downloadPdfBtn.addEventListener('click', async () => {
|
|
|
|
if (pdfDoc) {
|
|
|
|
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 targetElement = signatureCanvas;
|
|
|
|
const x = parseFloat(targetElement.getAttribute('data-x')) || 0;
|
|
|
|
const y = parseFloat(targetElement.getAttribute('data-y')) || 0;
|
|
|
|
const scale = parseFloat(targetElement.getAttribute('data-scale')) || 1;
|
|
|
|
|
|
|
|
page.drawImage(signatureImageObject, {
|
|
|
|
x: x,
|
|
|
|
y: page.getHeight() - y - (targetElement.height * scale),
|
|
|
|
width: targetElement.width * scale,
|
|
|
|
height: targetElement.height * scale,
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const modifiedPdfBytes = await pdfDocModified.save();
|
|
|
|
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
|
|
|
|
const link = document.createElement('a');
|
|
|
|
link.href = URL.createObjectURL(blob);
|
|
|
|
link.download = 'signed-document.pdf';
|
|
|
|
link.click();
|
2023-05-02 23:59:16 +02:00
|
|
|
}
|
2023-05-03 23:07:51 +02:00
|
|
|
});
|
2023-05-02 23:59:16 +02:00
|
|
|
|
2023-05-03 23:07:51 +02:00
|
|
|
async function dataURLToArrayBuffer(dataURL) {
|
|
|
|
const response = await fetch(dataURL);
|
|
|
|
return response.arrayBuffer();
|
2023-05-02 23:59:16 +02:00
|
|
|
}
|
2023-05-03 23:07:51 +02:00
|
|
|
|
2023-05-02 23:59:16 +02:00
|
|
|
});
|
2023-05-03 23:07:51 +02:00
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|