Add elements demo (WIP)

This commit is contained in:
Saud Fatayerji 2023-09-04 17:07:29 -07:00
parent b12c1305ea
commit e998426b3b
4 changed files with 542 additions and 101 deletions

View File

@ -137,6 +137,14 @@ public class GeneralWebController {
return "sign";
}
@GetMapping("/add-elements")
@Hidden
public String addElements(Model model) {
model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames());
return "add-elements";
}
@Autowired
private ResourceLoader resourceLoader;

View File

@ -24,6 +24,7 @@ alphabet=Alphabet
downloadPdf=Download PDF
text=Text
font=Font
element-id=Element ID
selectFillter=-- Select --
pageNum=Page Number
sizes.small=Small
@ -514,6 +515,17 @@ sign.text=Text Input
sign.clear=Clear
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.title=Repair

View File

@ -1,3 +1,4 @@
const DraggableUtils = {
boxDragContainer: document.getElementById('box-drag-container'),
@ -5,83 +6,50 @@ const DraggableUtils = {
nextId: 0,
pdfDoc: null,
pageIndex: 0,
documentsMap: new Map(),
documentsMap: new Map(), // {pdfDoc: {0: [addedElement, ...], 0-offsetWidth: 1920, 0-offsetHeight: 1080, ... }, ...}
init() {
interact('.draggable-canvas')
.draggable({
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);
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,
});
interact('.draggable-canvas.resizeable')
.draggable(this.draggableConfig)
.resizable(this.resizableConfig);
interact('.draggable-canvas:not(.resizeable)')
.draggable(this.draggableConfig);
},
onInteraction(target) {
if (!target.classList.contains("draggable-canvas")) {
return;
}
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() {
const createdCanvas = document.createElement('canvas');
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
createdCanvas.classList.add("draggable-canvas");
createdCanvas.classList.add("draggable-canvas", "resizeable");
const x = 0;
const y = 20;
@ -234,12 +202,105 @@ const DraggableUtils = {
const offsetHeight = pagesMap[pageIdx+"-offsetHeight"];
for (const draggableData of draggablesData) {
// embed the draggable canvas
const draggableElement = draggableData.element;
if (draggableElement.nodeName == "CANVAS") {
// embed the draggable canvas
const response = await fetch(draggableElement.toDataURL());
const draggableImgBytes = await response.arrayBuffer();
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
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
const transformComponents = tansform.split(",");
@ -250,31 +311,24 @@ const DraggableUtils = {
height: draggableData.offsetHeight,
};
const draggablePositionRelative = {
x: draggablePositionPixels.x / offsetWidth,
y: draggablePositionPixels.y / offsetHeight,
width: draggablePositionPixels.width / offsetWidth,
height: draggablePositionPixels.height / offsetHeight,
}
x: draggablePositionPixels.x / pageOffsetWidth,
y: draggablePositionPixels.y / pageOffsetHeight,
width: draggablePositionPixels.width / pageOffsetWidth,
height: draggablePositionPixels.height / pageOffsetHeight,
};
const draggablePositionPdf = {
x: draggablePositionRelative.x * page.getWidth(),
y: draggablePositionRelative.y * page.getHeight(),
width: draggablePositionRelative.width * page.getWidth(),
height: draggablePositionRelative.height * page.getHeight(),
}
// draw the image
page.drawImage(pdfImageObject, {
};
const translatedPositions = {
x: draggablePositionPdf.x,
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
});
height: draggablePositionPdf.width,
}
}
this.loadPageContents();
return pdfDocModified;
},
return translatedPositions;
}
document.addEventListener("DOMContentLoaded", () => {

View 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>