mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 07:55:07 +00:00
Add elements demo (WIP)
This commit is contained in:
parent
b12c1305ea
commit
e998426b3b
@ -137,6 +137,14 @@ public class GeneralWebController {
|
|||||||
return "sign";
|
return "sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/add-elements")
|
||||||
|
@Hidden
|
||||||
|
public String addElements(Model model) {
|
||||||
|
model.addAttribute("currentPage", "sign");
|
||||||
|
model.addAttribute("fonts", getFontNames());
|
||||||
|
return "add-elements";
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ResourceLoader resourceLoader;
|
private ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ alphabet=Alphabet
|
|||||||
downloadPdf=Download PDF
|
downloadPdf=Download PDF
|
||||||
text=Text
|
text=Text
|
||||||
font=Font
|
font=Font
|
||||||
|
element-id=Element ID
|
||||||
selectFillter=-- Select --
|
selectFillter=-- Select --
|
||||||
pageNum=Page Number
|
pageNum=Page Number
|
||||||
sizes.small=Small
|
sizes.small=Small
|
||||||
@ -514,6 +515,17 @@ sign.text=Text Input
|
|||||||
sign.clear=Clear
|
sign.clear=Clear
|
||||||
sign.add=Add
|
sign.add=Add
|
||||||
|
|
||||||
|
#add elements
|
||||||
|
add-elements.title=Add Elements
|
||||||
|
add-elements.header=Add Elements
|
||||||
|
add-elements.upload=Upload Image
|
||||||
|
add-elements.draw=Draw Signature
|
||||||
|
add-elements.text=Text Input
|
||||||
|
add-elements.textbox=Text Box
|
||||||
|
add-elements.checkbox=Check Box
|
||||||
|
add-elements.clear=Clear
|
||||||
|
add-elements.add=Add
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Repair
|
repair.title=Repair
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const DraggableUtils = {
|
const DraggableUtils = {
|
||||||
|
|
||||||
boxDragContainer: document.getElementById('box-drag-container'),
|
boxDragContainer: document.getElementById('box-drag-container'),
|
||||||
@ -5,83 +6,50 @@ const DraggableUtils = {
|
|||||||
nextId: 0,
|
nextId: 0,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
documentsMap: new Map(),
|
documentsMap: new Map(), // {pdfDoc: {0: [addedElement, ...], 0-offsetWidth: 1920, 0-offsetHeight: 1080, ... }, ...}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
interact('.draggable-canvas')
|
interact('.draggable-canvas.resizeable')
|
||||||
.draggable({
|
.draggable(this.draggableConfig)
|
||||||
listeners: {
|
.resizable(this.resizableConfig);
|
||||||
move: (event) => {
|
interact('.draggable-canvas:not(.resizeable)')
|
||||||
const target = event.target;
|
.draggable(this.draggableConfig);
|
||||||
const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
|
|
||||||
const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
|
|
||||||
|
|
||||||
target.style.transform = `translate(${x}px, ${y}px)`;
|
|
||||||
target.setAttribute('data-bs-x', x);
|
|
||||||
target.setAttribute('data-bs-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-bs-x')) || 0)
|
|
||||||
var y = (parseFloat(target.getAttribute('data-bs-y')) || 0)
|
|
||||||
|
|
||||||
// check if control key is pressed
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
const aspectRatio = target.offsetWidth / target.offsetHeight;
|
|
||||||
// preserve aspect ratio
|
|
||||||
let width = event.rect.width;
|
|
||||||
let height = event.rect.height;
|
|
||||||
|
|
||||||
if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
|
|
||||||
height = width / aspectRatio;
|
|
||||||
} else {
|
|
||||||
width = height * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.rect.width = width;
|
|
||||||
event.rect.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
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-bs-x', x)
|
|
||||||
target.setAttribute('data-bs-y', y)
|
|
||||||
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
|
|
||||||
|
|
||||||
this.onInteraction(target);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
modifiers: [
|
|
||||||
interact.modifiers.restrictSize({
|
|
||||||
min: { width: 5, height: 5 },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
inertia: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onInteraction(target) {
|
onInteraction(target) {
|
||||||
|
if (!target.classList.contains("draggable-canvas")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.boxDragContainer.appendChild(target);
|
this.boxDragContainer.appendChild(target);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addDraggableElement(element, resizable) {
|
||||||
|
const createdWrapper = document.createElement('div');
|
||||||
|
createdWrapper.id = `draggable-canvas-${this.nextId++}`;
|
||||||
|
createdWrapper.appendChild(element);
|
||||||
|
createdWrapper.classList.add("draggable-canvas");
|
||||||
|
if (resizable) {
|
||||||
|
createdWrapper.classList.add("resizeable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = 0;
|
||||||
|
const y = 50;
|
||||||
|
createdWrapper.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
|
createdWrapper.style.lineHeight = "0";
|
||||||
|
createdWrapper.setAttribute('data-bs-x', x);
|
||||||
|
createdWrapper.setAttribute('data-bs-y', y);
|
||||||
|
|
||||||
|
createdWrapper.onclick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.onInteraction(e.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.boxDragContainer.appendChild(createdWrapper);
|
||||||
|
return createdWrapper;
|
||||||
|
},
|
||||||
createDraggableCanvas() {
|
createDraggableCanvas() {
|
||||||
const createdCanvas = document.createElement('canvas');
|
const createdCanvas = document.createElement('canvas');
|
||||||
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
|
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
|
||||||
createdCanvas.classList.add("draggable-canvas");
|
createdCanvas.classList.add("draggable-canvas", "resizeable");
|
||||||
|
|
||||||
const x = 0;
|
const x = 0;
|
||||||
const y = 20;
|
const y = 20;
|
||||||
@ -234,12 +202,105 @@ const DraggableUtils = {
|
|||||||
const offsetHeight = pagesMap[pageIdx+"-offsetHeight"];
|
const offsetHeight = pagesMap[pageIdx+"-offsetHeight"];
|
||||||
|
|
||||||
for (const draggableData of draggablesData) {
|
for (const draggableData of draggablesData) {
|
||||||
// embed the draggable canvas
|
|
||||||
const draggableElement = draggableData.element;
|
const draggableElement = draggableData.element;
|
||||||
|
if (draggableElement.nodeName == "CANVAS") {
|
||||||
|
// embed the draggable canvas
|
||||||
const response = await fetch(draggableElement.toDataURL());
|
const response = await fetch(draggableElement.toDataURL());
|
||||||
const draggableImgBytes = await response.arrayBuffer();
|
const draggableImgBytes = await response.arrayBuffer();
|
||||||
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
|
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
|
||||||
|
|
||||||
|
const translatedPositions = this.rescaleForPage(page, draggableData, offsetWidth, offsetHeight);
|
||||||
|
|
||||||
|
// draw the image
|
||||||
|
page.drawImage(pdfImageObject, translatedPositions);
|
||||||
|
} else if (draggableElement.firstChild.nodeName == "INPUT" && draggableElement.firstChild.getAttribute("type") == "textarea") {
|
||||||
|
const translatedPositions = this.rescaleForPage(page, draggableData, offsetWidth, offsetHeight);
|
||||||
|
const fieldKey = draggableElement.firstChild.getAttribute("name");
|
||||||
|
const form = pdfDocModified.getForm();
|
||||||
|
const field = form.createTextField(fieldKey);
|
||||||
|
field.addToPage(page, translatedPositions);
|
||||||
|
} else if (draggableElement.firstChild.nodeName == "INPUT" && draggableElement.firstChild.getAttribute("type") == "checkbox") {
|
||||||
|
const translatedPositions = this.rescaleForPage(page, draggableData, offsetWidth, offsetHeight);
|
||||||
|
const fieldKey = draggableElement.firstChild.getAttribute("name");
|
||||||
|
const form = pdfDocModified.getForm();
|
||||||
|
const field = form.createCheckBox(fieldKey);
|
||||||
|
field.addToPage(page, translatedPositions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadPageContents();
|
||||||
|
return pdfDocModified;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DraggableUtils.draggableConfig = {
|
||||||
|
listeners: {
|
||||||
|
move: (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
|
||||||
|
const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
|
||||||
|
|
||||||
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
|
target.setAttribute('data-bs-x', x);
|
||||||
|
target.setAttribute('data-bs-y', y);
|
||||||
|
|
||||||
|
DraggableUtils.onInteraction(target);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DraggableUtils.resizableConfig = {
|
||||||
|
edges: { left: true, right: true, bottom: true, top: true },
|
||||||
|
listeners: {
|
||||||
|
move: (event) => {
|
||||||
|
var target = event.target
|
||||||
|
var x = (parseFloat(target.getAttribute('data-bs-x')) || 0)
|
||||||
|
var y = (parseFloat(target.getAttribute('data-bs-y')) || 0)
|
||||||
|
|
||||||
|
// check if control key is pressed
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
const aspectRatio = target.offsetWidth / target.offsetHeight;
|
||||||
|
// preserve aspect ratio
|
||||||
|
let width = event.rect.width;
|
||||||
|
let height = event.rect.height;
|
||||||
|
|
||||||
|
if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
|
||||||
|
height = width / aspectRatio;
|
||||||
|
} else {
|
||||||
|
width = height * aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.rect.width = width;
|
||||||
|
event.rect.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
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-bs-x', x)
|
||||||
|
target.setAttribute('data-bs-y', y)
|
||||||
|
//target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
|
||||||
|
|
||||||
|
DraggableUtils.onInteraction(target);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
modifiers: [
|
||||||
|
interact.modifiers.restrictSize({
|
||||||
|
min: { width: 5, height: 5 },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
inertia: true,
|
||||||
|
},
|
||||||
|
DraggableUtils.rescaleForPage = (page, draggableData, pageOffsetWidth, pageOffsetHeight) => {
|
||||||
|
const draggableElement = draggableData.element;
|
||||||
|
|
||||||
// calculate the position in the pdf document
|
// calculate the position in the pdf document
|
||||||
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
|
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
|
||||||
const transformComponents = tansform.split(",");
|
const transformComponents = tansform.split(",");
|
||||||
@ -250,31 +311,24 @@ const DraggableUtils = {
|
|||||||
height: draggableData.offsetHeight,
|
height: draggableData.offsetHeight,
|
||||||
};
|
};
|
||||||
const draggablePositionRelative = {
|
const draggablePositionRelative = {
|
||||||
x: draggablePositionPixels.x / offsetWidth,
|
x: draggablePositionPixels.x / pageOffsetWidth,
|
||||||
y: draggablePositionPixels.y / offsetHeight,
|
y: draggablePositionPixels.y / pageOffsetHeight,
|
||||||
width: draggablePositionPixels.width / offsetWidth,
|
width: draggablePositionPixels.width / pageOffsetWidth,
|
||||||
height: draggablePositionPixels.height / offsetHeight,
|
height: draggablePositionPixels.height / pageOffsetHeight,
|
||||||
}
|
};
|
||||||
const draggablePositionPdf = {
|
const draggablePositionPdf = {
|
||||||
x: draggablePositionRelative.x * page.getWidth(),
|
x: draggablePositionRelative.x * page.getWidth(),
|
||||||
y: draggablePositionRelative.y * page.getHeight(),
|
y: draggablePositionRelative.y * page.getHeight(),
|
||||||
width: draggablePositionRelative.width * page.getWidth(),
|
width: draggablePositionRelative.width * page.getWidth(),
|
||||||
height: draggablePositionRelative.height * page.getHeight(),
|
height: draggablePositionRelative.height * page.getHeight(),
|
||||||
}
|
};
|
||||||
|
const translatedPositions = {
|
||||||
// draw the image
|
|
||||||
page.drawImage(pdfImageObject, {
|
|
||||||
x: draggablePositionPdf.x,
|
x: draggablePositionPdf.x,
|
||||||
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
|
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
|
||||||
width: draggablePositionPdf.width,
|
width: draggablePositionPdf.width,
|
||||||
height: draggablePositionPdf.height,
|
height: draggablePositionPdf.width,
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
return translatedPositions;
|
||||||
|
|
||||||
this.loadPageContents();
|
|
||||||
return pdfDocModified;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
367
src/main/resources/templates/add-elements.html
Normal file
367
src/main/resources/templates/add-elements.html
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
<!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=#{add-elements.title})}"></th:block>
|
||||||
|
<script src="js/thirdParty/signature_pad.umd.min.js"></script>
|
||||||
|
<script src="js/thirdParty/interact.min.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<th:block th:each="font : ${fonts}">
|
||||||
|
<style th:inline="text">
|
||||||
|
@font-face {
|
||||||
|
font-family: "[[${font}]]";
|
||||||
|
src: url('fonts/[[${font}]].woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
#font-select option[value="[[${font}]]"] {
|
||||||
|
font-family: "[[${font}]]", cursive;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</th:block>
|
||||||
|
<style>
|
||||||
|
select#font-select, select#font-select option {
|
||||||
|
height: 60px; /* Adjust as needed */
|
||||||
|
font-size: 30px; /* Adjust as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<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="#{add-elements.header}"></h2>
|
||||||
|
|
||||||
|
<!-- pdf selector -->
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
|
||||||
|
<script>
|
||||||
|
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/pdf.worker.js'
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- add elements UIs -->
|
||||||
|
<div class="add-element-list">
|
||||||
|
<button id="addCheckBoxButton">Add Checkbox</button>
|
||||||
|
<script>
|
||||||
|
document.getElementById("addCheckBoxButton").addEventListener("click", () => {
|
||||||
|
const inp = document.createElement('input');
|
||||||
|
inp.setAttribute("type", "checkbox");
|
||||||
|
DraggableUtils.addDraggableElement(inp, false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<div class="tab-group show-on-file-selected">
|
||||||
|
<div class="tab-container" th:title="#{add-elements.upload}">
|
||||||
|
<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 class="tab-container drawing-pad-container" th:title="#{add-elements.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) {
|
||||||
|
// code is from: https://github.com/szimek/signature_pad/issues/49#issuecomment-1104035775
|
||||||
|
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() {
|
||||||
|
// When zoomed out to less than 100%, for some very strange reason,
|
||||||
|
// some browsers report devicePixelRatio as less than 1
|
||||||
|
// and only part of the canvas is cleared then.
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
// This library does not listen for canvas changes, so after the canvas is automatically
|
||||||
|
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
|
||||||
|
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
|
||||||
|
// that the state of this library is consistent with visual state of the canvas, you
|
||||||
|
// have to clear it manually.
|
||||||
|
signaturePad.clear();
|
||||||
|
}
|
||||||
|
new IntersectionObserver((entries, observer) => {
|
||||||
|
if (entries.some(entry => entry.intersectionRatio > 0)) {
|
||||||
|
resizeCanvas();
|
||||||
|
}
|
||||||
|
}).observe(signaturePadCanvas);
|
||||||
|
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.drawing-pad-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drawing-pad-canvas {
|
||||||
|
background: rgba(125,125,125,0.2);
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
<div class="tab-container" th:title="#{add-elements.text}">
|
||||||
|
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
||||||
|
<input type="text" class="form-control" id="sigText" name="sigText">
|
||||||
|
<label th:text="#{font}"></label>
|
||||||
|
<select class="form-control" name="font" id="font-select">
|
||||||
|
<option th:each="font : ${fonts}" th:value="${font}" th:text="${font}" th:class="${font.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;
|
||||||
|
|
||||||
|
canvas.width = textWidth;
|
||||||
|
canvas.height = textHeight*1.35; //for tails
|
||||||
|
ctx.font = `${fontSize}px ${font}`;
|
||||||
|
ctx.fillText(sigText, 0, fontSize);
|
||||||
|
|
||||||
|
const dataURL = canvas.toDataURL();
|
||||||
|
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
const sigTextInput = document.getElementById('sigText');
|
||||||
|
const fontSelect = document.getElementById('font-select');
|
||||||
|
|
||||||
|
const updateOptionTexts = () => {
|
||||||
|
Array.from(fontSelect.options).forEach(option => {
|
||||||
|
const fontName = option.value.replace(/-regular$/i, '');
|
||||||
|
option.text = sigTextInput.value || fontName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sigTextInput.addEventListener('input', updateOptionTexts);
|
||||||
|
|
||||||
|
fontSelect.addEventListener('change', (e) => {
|
||||||
|
e.target.style.fontFamily = e.target.value;
|
||||||
|
updateOptionTexts();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually trigger the change event
|
||||||
|
fontSelect.dispatchEvent(new Event('change'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<th:block th:each="font : ${fonts}">
|
||||||
|
<style th:inline="text">
|
||||||
|
#font-select option[value="/*[[${font}]]*/"] {
|
||||||
|
font-family: '/*[[${font}]]*/', cursive;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="tab-container" th:title="#{add-elements.textbox}">
|
||||||
|
<label class="form-check-label" for="textboxId" th:text="#{element-id}"></label>
|
||||||
|
<input type="text" class="form-control" id="textboxId" name="textboxId">
|
||||||
|
<div class="margin-auto-parent">
|
||||||
|
<button id="save-textbox" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromTextBox()" th:text="#{sign.add}"></button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function addDraggableFromTextBox() {
|
||||||
|
const id = document.getElementById('textboxId').value;
|
||||||
|
|
||||||
|
const textbox = document.createElement('input');
|
||||||
|
textbox.setAttribute('type', 'textarea');
|
||||||
|
textbox.setAttribute('name', id);
|
||||||
|
textbox.style.width = "100%";
|
||||||
|
textbox.style.height = "100%";
|
||||||
|
textbox.readonly = true;
|
||||||
|
|
||||||
|
const wrapper = DraggableUtils.addDraggableElement(textbox, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<div class="tab-container" th:title="#{add-elements.checkbox}">
|
||||||
|
<label class="form-check-label" for="checkboxId" th:text="#{element-id}"></label>
|
||||||
|
<input type="text" class="form-control" id="checkboxId" name="checkboxId">
|
||||||
|
<div class="margin-auto-parent">
|
||||||
|
<button id="save-checkbox" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromCheckBox()" th:text="#{sign.add}"></button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function addDraggableFromCheckBox() {
|
||||||
|
const id = document.getElementById('checkboxId').value;
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.setAttribute('type', 'checkbox');
|
||||||
|
checkbox.setAttribute('name', id);
|
||||||
|
const wrapper = DraggableUtils.addDraggableElement(checkbox, false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- draggables box -->
|
||||||
|
<div id="box-drag-container" class="show-on-file-selected">
|
||||||
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
<script 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('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;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.draggable-buttons-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.draggable-buttons-box > button {
|
||||||
|
z-index: 10;
|
||||||
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
|
}
|
||||||
|
.draggable-canvas {
|
||||||
|
border: 1px solid red;
|
||||||
|
position: absolute;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
top: 0px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- download button -->
|
||||||
|
<div class="margin-auto-parent">
|
||||||
|
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user