Compare commits

...

6 Commits

Author SHA1 Message Date
Anthony Stirling
217bba3a9d Merge branch 'main' into add-elements 2023-12-30 20:17:41 +00:00
Anthony Stirling
0e9f52bca2 Merge branch 'main' into add-elements 2023-12-28 23:52:45 +00:00
Anthony Stirling
4f6286845d Api fix 2023-12-28 22:52:53 +00:00
Anthony Stirling
5d611a2fa3 Merge remote-tracking branch 'origin/main' into add-elements 2023-12-27 15:26:10 +00:00
Saud Fatayerji
86984f2142 Add elements first draft 2023-09-28 19:31:43 +03:00
Saud Fatayerji
e998426b3b Add elements demo (WIP) 2023-09-04 17:07:29 -07:00
8 changed files with 794 additions and 104 deletions

View File

@@ -56,7 +56,6 @@ public class MetricsFilter extends OncePerRequestFilter {
.register(meterRegistry);
counter.increment();
// System.out.println("Counted");
}
filterChain.doFilter(request, response);

View File

@@ -160,6 +160,14 @@ public class GeneralWebController {
return "sign";
}
@GetMapping("/add-elements")
@Hidden
public String addElements(Model model) {
model.addAttribute("currentPage", "add-elements");
model.addAttribute("fonts", getFontNames());
return "add-elements";
}
@GetMapping("/multi-page-layout")
@Hidden
public String multiPageLayoutForm(Model model) {

View File

@@ -197,6 +197,7 @@ public class MetricsController {
for (Meter meter : meterRegistry.getMeters()) {
if (meter.getId().getName().equals("http.requests")) {
String method = meter.getId().getTag("method");
System.out.println("method=" + method + ", endpont=" + endpoint.get());
if (method != null && method.equals("POST")) {
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
if (!endpoint.get().startsWith("/")) {
@@ -233,10 +234,15 @@ public class MetricsController {
Map<String, Double> counts = new HashMap<>();
for (Meter meter : meterRegistry.getMeters()) {
System.out.println("meter.getId().getName()=" + meter.getId().getName());
if (meter.getId().getName().equals("http.requests")) {
String method = meter.getId().getTag("method");
System.out.println("method=" + method );
if (method != null && method.equals("POST")) {
String uri = meter.getId().getTag("uri");
String uri = meter.getId().getTag("uri");
System.out.println("method=" + method + ", endpont=" + meter.getId());
if (uri != null) {
double currentCount = counts.getOrDefault(uri, 0.0);
if (meter instanceof Counter) {

View File

@@ -24,6 +24,10 @@ alphabet=Alphabet
downloadPdf=Download PDF
text=Text
font=Font
element-id=Element ID
group-id=Group ID
comma-separated-values=Element values (separated by commas)
element-value=Element value
selectFillter=-- Select --
pageNum=Page Number
sizes.small=Small
@@ -567,6 +571,20 @@ 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.checkbox=Check Box
add-elements.dropdown=Dropdown
add-elements.optionList=Option List
add-elements.radioButton=Radio Button
add-elements.textbox=Text 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;
@@ -216,7 +184,9 @@ const DraggableUtils = {
parseTransform(element) {
},
async getOverlayedPdfDocument() {
async getOverlaidPdfDocument() {
var radioGroups = new Map();
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true });
this.storePageContents();
@@ -234,41 +204,58 @@ const DraggableUtils = {
const offsetHeight = pagesMap[pageIdx+"-offsetHeight"];
for (const draggableData of draggablesData) {
// embed the draggable canvas
const draggableElement = draggableData.element;
const response = await fetch(draggableElement.toDataURL());
const draggableImgBytes = await response.arrayBuffer();
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
// calculate the position in the pdf document
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
const transformComponents = tansform.split(",");
const draggablePositionPixels = {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
width: draggableData.offsetWidth,
height: draggableData.offsetHeight,
};
const draggablePositionRelative = {
x: draggablePositionPixels.x / offsetWidth,
y: draggablePositionPixels.y / offsetHeight,
width: draggablePositionPixels.width / offsetWidth,
height: draggablePositionPixels.height / offsetHeight,
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") == "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);
} else if (draggableElement.firstChild.nodeName == "DIV" && draggableElement.firstChild.getAttribute("type") == "dropdown") {
const translatedPositions = this.rescaleForPage(page, draggableData, offsetWidth, offsetHeight);
const fieldKey = draggableElement.firstChild.getAttribute("name");
const fieldValues = JSON.parse(draggableElement.firstChild.getAttribute("values"));
const form = pdfDocModified.getForm();
const field = form.createDropdown(fieldKey);
field.addOptions(fieldValues)
field.addToPage(page, translatedPositions);
} else if (draggableElement.firstChild.nodeName == "DIV" && draggableElement.firstChild.getAttribute("type") == "optionList") {
const translatedPositions = this.rescaleForPage(page, draggableData, offsetWidth, offsetHeight);
const fieldKey = draggableElement.firstChild.getAttribute("name");
const fieldValues = JSON.parse(draggableElement.firstChild.getAttribute("values"));
const form = pdfDocModified.getForm();
const field = form.createOptionList(fieldKey);
field.addOptions(fieldValues)
field.addToPage(page, translatedPositions);
} else if (draggableElement.firstChild.nodeName == "INPUT" && draggableElement.firstChild.getAttribute("type") == "radio") { //
const translatedPositions = this.rescaleForPage(page, draggableData, offsetWidth, offsetHeight);
const fieldKey = draggableElement.firstChild.getAttribute("name");
const buttonValue = draggableElement.firstChild.getAttribute("buttonValue");
const form = pdfDocModified.getForm();
var radioGroup = radioGroups.get(fieldKey);
if (!radioGroup) {
radioGroup = form.createRadioGroup(fieldKey);
radioGroups.set(fieldKey, radioGroup);
}
radioGroup.addOptionToPage(buttonValue, page, translatedPositions)
console.log("added radio")
} 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);
}
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, {
x: draggablePositionPdf.x,
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
});
}
}
@@ -277,6 +264,103 @@ const DraggableUtils = {
},
}
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(",");
const draggablePositionPixels = {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
width: draggableData.offsetWidth,
height: draggableData.offsetHeight,
};
const draggablePositionRelative = {
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(),
};
const translatedPositions = {
x: draggablePositionPdf.x,
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width,
height: draggablePositionPdf.width,
}
return translatedPositions;
}
document.addEventListener("DOMContentLoaded", () => {
DraggableUtils.init();
});

View File

@@ -0,0 +1,434 @@
<!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="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 + '-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.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 class="tab-container" th:title="#{add-elements.dropdown}">
<label class="form-check-label" for="dropdownId" th:text="#{element-id}"></label>
<input type="text" class="form-control" id="dropdownId" name="dropdownId">
<label class="form-check-label" for="dropdownValues" th:text="#{comma-separated-values}"></label>
<input type="text" class="form-control" id="dropdownValues" name="dropdownValues">
<div class="margin-auto-parent">
<button id="save-dropdown" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromDropdown()" th:text="#{sign.add}"></button>
</div>
<script th:inline="javascript">
function addDraggableFromDropdown() {
const dropdownLabel = /*[[#{add-elements.dropdown}]]*/ '';
const id = document.getElementById('dropdownId').value;
const values = document.getElementById('dropdownValues').value.split(",");
const dropdown = document.createElement('div');
dropdown.setAttribute('type', 'dropdown');
dropdown.setAttribute('name', id);
dropdown.setAttribute('values', JSON.stringify(values));
dropdown.innerHTML = `${dropdownLabel}: ${id}`;
const wrapper = DraggableUtils.addDraggableElement(dropdown, true);
}
</script>
</div>
<div class="tab-container" th:title="#{add-elements.optionList}">
<label class="form-check-label" for="optionListId" th:text="#{element-id}"></label>
<input type="text" class="form-control" id="optionListId" name="optionListId">
<label class="form-check-label" for="optionListValues" th:text="#{comma-separated-values}"></label>
<input type="text" class="form-control" id="optionListValues" name="optionListValues">
<div class="margin-auto-parent">
<button id="save-optionList" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromoptionList()" th:text="#{sign.add}"></button>
</div>
<script th:inline="javascript">
function addDraggableFromoptionList() {
const optionListLabel = /*[[#{add-elements.optionList}]]*/ '';
const id = document.getElementById('optionListId').value;
const values = document.getElementById('optionListValues').value.split(",");
const optionList = document.createElement('div');
optionList.setAttribute('type', 'optionList');
optionList.setAttribute('name', id);
optionList.setAttribute('values', JSON.stringify(values));
optionList.innerHTML = `${optionListLabel}: ${id}`;
const wrapper = DraggableUtils.addDraggableElement(optionList, true);
}
</script>
</div>
<div class="tab-container" th:title="#{add-elements.radioButton}">
<label class="form-check-label" for="radioButtonId" th:text="#{group-id}"></label>
<input type="text" class="form-control" id="radioButtonId" name="radioButton">
<label class="form-check-label" for="radioButtonValue" th:text="#{element-value}"></label>
<input type="text" class="form-control" id="radioButtonValue" name="radioButton">
<div class="margin-auto-parent">
<button id="save-radioButton" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromRadioButton()" th:text="#{sign.add}"></button>
</div>
<script>
function addDraggableFromRadioButton() {
const id = document.getElementById('radioButtonId').value;
const value = document.getElementById('radioButtonValue').value;
const radioButton = document.createElement('input');
radioButton.setAttribute('type', 'radio');
radioButton.setAttribute('name', id);
radioButton.setAttribute('buttonValue', value);
const wrapper = DraggableUtils.addDraggableElement(radioButton, false);
}
</script>
</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>
<!-- 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;
}
.draggable-canvas > div {
width: 100%;
height: 100%;
line-height: normal;
color: rgb(var(--base-font-color));
background-color: rgba(var(--body-background-color), 0.5);
}
</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.getOverlaidPdfDocument();
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>

View File

@@ -0,0 +1,141 @@
<!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=#{addImage.title})}"></th:block>
<script src="js/thirdParty/interact.min.js"></script>
</head>
<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="#{addImage.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>
<div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{addImage.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>
<!-- 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;
}
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
}
.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.getOverlaidPdfDocument();
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 + '_addedImage.pdf';
link.click();
});
</script>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -314,7 +314,7 @@ select#font-select, select#font-select option {
<script>
document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdf = await DraggableUtils.getOverlaidPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');