1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-11-14 03:20:14 +01:00
Stirling-PDF/src/main/resources/templates/sign.html

390 lines
18 KiB
HTML
Raw Normal View History

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>
2023-05-06 15:04:55 +02:00
<th:block th:insert="~{fragments/common :: head(title=#{sign.title})}"></th:block>
2023-05-02 23:59:16 +02:00
<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>
2023-05-06 01:48:56 +02:00
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
2023-05-02 23:59:16 +02:00
<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;
}
2023-05-02 23:59:16 +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-06 15:32:38 +02:00
#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;
}
2023-05-02 23:59:16 +02:00
</style>
</head>
<body>
<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">
2023-05-06 15:04:55 +02:00
<h2 th:text="#{sign.header}"></h2>
<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>
2023-05-06 01:48:56 +02:00
<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">
2023-05-06 01:48:56 +02:00
<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">
<label class="btn btn-outline-secondary" for="type-signature">Type</label>
2023-05-02 23:59:16 +02:00
</div>
<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 -->
<div id="signature-pad-container">
2023-05-02 23:59:16 +02:00
<canvas id="signature-pad-canvas"></canvas>
<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:22:33 +02:00
2023-05-06 01:48:56 +02:00
<div id="signature-type-container">
<label class="form-check-label" for="sigText">Signature</label>
<input type="text" class="form-control" id="sigText" name="sigText">
<label>Font:</label>
<select class="form-control" name="font" id="font-select">
<option value="Estonia" class="estonia-font">Estonia</option>
<option value="Tangerine" class="tangerine-font">Tangerine</option>
<option value="Windsong" class="windsong-font">Windsong</option>
</select>
<button id="save-text-signature" class="btn btn-outline-success mt-2">Save</button>
</div>
<div id="pdf-container">
<canvas id="pdf-canvas"></canvas>
2023-05-06 18:56:31 +02:00
<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>
document.addEventListener('DOMContentLoaded', () => {
2023-05-06 01:48:56 +02:00
const fontSelect = document.getElementById('font-select');
2023-05-06 15:32:38 +02:00
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');
2023-05-06 01:48:56 +02:00
const signatureTypeContainer = document.getElementById('signature-type-container');
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = "none"
2023-05-06 01:48:56 +02:00
signatureTypeContainer.style.display = 'none';
const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1,
maxWidth: 2,
penColor: 'black',
});
2023-05-02 23:59:16 +02:00
const pdfCtx = pdfCanvas.getContext('2d');
let pdfDoc = null;
2023-05-02 23:59:16 +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
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
clearSignatureBtn.addEventListener('click', () => {
signaturePad.clear();
});
2023-05-02 23:59:16 +02:00
2023-05-05 00:22:33 +02:00
$("input[name=signature-type]").change(function() {
const drawSignatureInput = document.getElementById('draw-signature');
2023-05-06 01:48:56 +02:00
const importImageInput = document.getElementById('import-image');
const typeSignatureInput = document.getElementById('type-signature');
2023-05-05 00:22:33 +02:00
signaturePadContainer.style.display = drawSignatureInput.checked ? 'block' : 'none';
2023-05-06 01:48:56 +02:00
signatureTypeContainer.style.display = typeSignatureInput.checked ? 'block' : 'none';
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = importImageInput.checked ? 'block' : 'none';
2023-05-02 23:59:16 +02:00
if (drawSignatureInput.checked) {
populateSignatureFromPad();
2023-05-06 01:48:56 +02:00
} else if (importImageInput.checked) {
populateSignatureFromFileUpload();
}
});
2023-05-06 01:48:56 +02:00
const saveTextSignatureBtn = document.getElementById('save-text-signature');
saveTextSignatureBtn.addEventListener('click', () => {
if (!document.getElementById('type-signature').checked) return;
const sigText = document.getElementById('sigText').value;
const font = document.querySelector('select[name=font]').value;
const fontSize = 50;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${fontSize}px ${font}`;
const textWidth = ctx.measureText(sigText).width;
const textHeight = fontSize;
canvas.width = textWidth;
canvas.height = textHeight*1.35; //for tails
ctx.font = `${fontSize}px ${font}`;
ctx.fillText(sigText, 0, fontSize);
const dataURL = canvas.toDataURL();
populateSignature(dataURL);
});
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;
const x = 0;
const y = 0;
signatureCanvas.style.transform = `translate(${x}px, ${y}px)`;
2023-05-06 18:56:31 +02:00
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
};
img.src = imgUrl;
}
function populateSignatureFromFileUpload() {
const file = signatureUpload.files[0];
if (!file) return;
2023-05-02 23:59:16 +02:00
const reader = new FileReader();
reader.onload = (e) => populateSignature(e.target.result);
reader.readAsDataURL(file);
}
function populateSignatureFromPad() {
2023-05-06 01:48:56 +02:00
if (!document.getElementById('draw-signature').checked) return;
if (signaturePad.isEmpty()) return;
2023-05-02 23:59:16 +02:00
const dataURL = signaturePad.toDataURL();
populateSignature(dataURL);
}
signatureUpload.addEventListener('change', populateSignatureFromFileUpload);
saveSignatureBtn.addEventListener('click', populateSignatureFromPad);
2023-05-02 23:59:16 +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
const renderCtx = {
canvasContext: pdfCtx,
viewport: viewport,
};
2023-05-02 23:59:16 +02:00
page.render(renderCtx);
});
}
2023-05-02 23:59:16 +02:00
function 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,
}
}
interact('#signature-canvas')
.draggable({
listeners: {
move: (event) => {
2023-05-06 18:56:31 +02:00
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) => {
2023-05-06 18:56:31 +02:00
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
2023-05-06 18:56:31 +02:00
target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
2023-05-06 18:56:31 +02:00
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,
});
2023-05-02 23:59:16 +02:00
async function getSignatureImage() {
const dataURL = signatureCanvas.toDataURL();
return dataURLToArrayBuffer(dataURL);
}
2023-05-02 23:59:16 +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 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 = {
x: signatureCanvasPositionRelative.x * page.getWidth(),
y: signatureCanvasPositionRelative.y * page.getHeight(),
width: signatureCanvasPositionRelative.width * page.getWidth(),
height: signatureCanvasPositionRelative.height * page.getHeight(),
}
page.drawImage(signatureImageObject, {
x: signaturePositionPdf.x,
y: page.getHeight() - signaturePositionPdf.y - signaturePositionPdf.height,
width: signaturePositionPdf.width,
height: signaturePositionPdf.height,
});
}
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-02 23:59:16 +02:00
async function dataURLToArrayBuffer(dataURL) {
const response = await fetch(dataURL);
return response.arrayBuffer();
2023-05-02 23:59:16 +02:00
}
2023-05-02 23:59:16 +02:00
});
</script>
</body>
</html>