Merge branch 'cleanups' of git@github.com:Frooodle/Stirling-PDF.git into cleanups
This commit is contained in:
commit
7353d69f1a
18 changed files with 761 additions and 373 deletions
|
@ -40,6 +40,8 @@ Feel free to request any features or bug fixes either in github issues or our [D
|
||||||
- Parallel file processing and downloads
|
- Parallel file processing and downloads
|
||||||
- API for integration with external scripts
|
- API for integration with external scripts
|
||||||
|
|
||||||
|
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
|
||||||
|
|
||||||
## Technologies used
|
## Technologies used
|
||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- PDFBox
|
- PDFBox
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
|
||||||
|
@ -67,9 +69,6 @@ public class GeneralWebController {
|
||||||
return "split-pdfs";
|
return "split-pdfs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/sign")
|
@GetMapping("/sign")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String signForm(Model model) {
|
public String signForm(Model model) {
|
||||||
|
@ -77,4 +76,19 @@ public class GeneralWebController {
|
||||||
return "sign";
|
return "sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
|
@ResponseBody
|
||||||
|
public String getRobotsTxt() {
|
||||||
|
String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISABILITY");
|
||||||
|
if (allowGoogleVisibility == null)
|
||||||
|
allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISABILITY");
|
||||||
|
if (allowGoogleVisibility == null)
|
||||||
|
allowGoogleVisibility = "true";
|
||||||
|
if (Boolean.parseBoolean(allowGoogleVisibility)) {
|
||||||
|
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nDisallow: /";
|
||||||
|
} else {
|
||||||
|
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,12 @@ public class OtherWebController {
|
||||||
return "other/change-metadata";
|
return "other/change-metadata";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/compare")
|
||||||
|
@Hidden
|
||||||
|
public String compareForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "compare");
|
||||||
|
return "other/compare";
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = "/usr/share/tesseract-ocr/4.00/tessdata";
|
String tessdataDir = "/usr/share/tesseract-ocr/4.00/tessdata";
|
||||||
|
|
|
@ -117,8 +117,25 @@ home.flatten.desc=Remove all interactive elements and forms from a PDF
|
||||||
home.repair.title=Repair
|
home.repair.title=Repair
|
||||||
home.repair.desc=Tries to repair a corrupt/broken PDF
|
home.repair.desc=Tries to repair a corrupt/broken PDF
|
||||||
|
|
||||||
|
downloadPdf=Download PDF
|
||||||
|
text=Text
|
||||||
|
font=Font
|
||||||
|
|
||||||
sign.title=Sign
|
sign.title=Sign
|
||||||
sign.header=Sign PDFs
|
sign.header=Sign PDFs
|
||||||
|
sign.upload=Upload Image
|
||||||
|
sign.draw=Draw Signature
|
||||||
|
sign.text=Text Input
|
||||||
|
sign.clear=Clear
|
||||||
|
sign.add=Add
|
||||||
|
|
||||||
|
repair.title=Repair
|
||||||
|
repair.header=Repair PDFs
|
||||||
|
repair.submit=Repair
|
||||||
|
|
||||||
|
flatten.title=Flatten
|
||||||
|
flatten.header=Flatten PDFs
|
||||||
|
flatten.submit=Flatten
|
||||||
|
|
||||||
ScannerImageSplit.selectText.1=Angle Threshold:
|
ScannerImageSplit.selectText.1=Angle Threshold:
|
||||||
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
|
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
/* Dark Mode Styles */
|
/* Dark Mode Styles */
|
||||||
body {
|
body {
|
||||||
background-color: #333 !important;
|
--body-background-color: 51, 51, 51;
|
||||||
color: #fff !important;
|
--base-font-color: 255, 255, 255;
|
||||||
|
background-color: rgb(var(--body-background-color)) !important;
|
||||||
|
color: rgb(var(--base-font-color)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-card {
|
.dark-card {
|
||||||
background-color: #333 !important;
|
background-color: rgb(var(--body-background-color)) !important;
|
||||||
color: white !important;
|
color: rgb(var(--base-font-color)) !important;
|
||||||
}
|
}
|
||||||
.jumbotron {
|
.jumbotron {
|
||||||
background-color: #222; /* or any other dark color */
|
background-color: #222; /* or any other dark color */
|
||||||
color: #fff !important; /* or any other light color */
|
color: rgb(var(--base-font-color)) !important; /* or any other light color */
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group {
|
.list-group {
|
||||||
background-color: #222 !important;
|
background-color: #222 !important;
|
||||||
color: fff !important;
|
color: rgb(var(--base-font-color)) !important;
|
||||||
}
|
}
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background-color: #222 !important;
|
background-color: #222 !important;
|
||||||
color: fff !important;
|
color: rgb(var(--base-font-color)) !important;
|
||||||
}
|
}
|
||||||
#support-section {
|
#support-section {
|
||||||
background-color: #444 !important;
|
background-color: #444 !important;
|
||||||
|
@ -30,5 +32,5 @@ body {
|
||||||
--background-color: rgba(255, 255, 255, 0.046) !important;
|
--background-color: rgba(255, 255, 255, 0.046) !important;
|
||||||
--scroll-bar-color: #4c4c4c !important;
|
--scroll-bar-color: #4c4c4c !important;
|
||||||
--scroll-bar-thumb: #d3d3d3 !important;
|
--scroll-bar-thumb: #d3d3d3 !important;
|
||||||
--scroll-bar-thumb-hover: #ffffff !important;
|
--scroll-bar-thumb-hover: rgb(var(--base-font-color)) !important;
|
||||||
}
|
}
|
|
@ -16,11 +16,14 @@
|
||||||
html[lang-direction=ltr] * {
|
html[lang-direction=ltr] * {
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[lang-direction=rtl] * {
|
html[lang-direction=rtl] * {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
.ignore-rtl {
|
||||||
|
direction: ltr !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
.align-top {
|
.align-top {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -47,3 +50,11 @@ html[lang-direction=rtl] * {
|
||||||
border-top-left-radius: 0.25rem !important;
|
border-top-left-radius: 0.25rem !important;
|
||||||
border-bottom-left-radius: 0.25rem !important;
|
border-bottom-left-radius: 0.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-auto-parent {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.margin-center {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
5
src/main/resources/static/css/light-mode.css
Normal file
5
src/main/resources/static/css/light-mode.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/* Dark Mode Styles */
|
||||||
|
body {
|
||||||
|
--body-background-color: 255, 255, 255;
|
||||||
|
--base-font-color: 33, 37, 41;
|
||||||
|
}
|
26
src/main/resources/static/css/tab-container.css
Normal file
26
src/main/resources/static/css/tab-container.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
.tab-group {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-container.active {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid rgba(var(--base-font-color), 0.25);
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.tab-buttons > button {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
background: 0 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: rgb(var(--base-font-color));
|
||||||
|
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
.tab-buttons > button.active {
|
||||||
|
background-color: rgb(var(--body-background-color));
|
||||||
|
border-color: rgba(var(--base-font-color), 0.25) rgba(var(--base-font-color), 0.25) rgb(var(--body-background-color));
|
||||||
|
}
|
BIN
src/main/resources/static/fonts/Estonia.woff2
Normal file
BIN
src/main/resources/static/fonts/Estonia.woff2
Normal file
Binary file not shown.
BIN
src/main/resources/static/fonts/Tangerine.woff2
Normal file
BIN
src/main/resources/static/fonts/Tangerine.woff2
Normal file
Binary file not shown.
211
src/main/resources/static/js/draggable-utils.js
Normal file
211
src/main/resources/static/js/draggable-utils.js
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
const DraggableUtils = {
|
||||||
|
|
||||||
|
boxDragContainer: document.getElementById('box-drag-container'),
|
||||||
|
pdfCanvas: document.getElementById('pdf-canvas'),
|
||||||
|
nextId: 0,
|
||||||
|
pdfDoc: null,
|
||||||
|
pageIndex: 0,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
interact('.draggable-canvas')
|
||||||
|
.draggable({
|
||||||
|
listeners: {
|
||||||
|
move: (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
|
||||||
|
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
|
||||||
|
|
||||||
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
|
target.setAttribute('data-x', x);
|
||||||
|
target.setAttribute('data-y', y);
|
||||||
|
|
||||||
|
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-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
|
||||||
|
|
||||||
|
target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
|
||||||
|
|
||||||
|
target.setAttribute('data-x', x)
|
||||||
|
target.setAttribute('data-y', y)
|
||||||
|
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
|
||||||
|
|
||||||
|
this.onInteraction(target);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modifiers: [
|
||||||
|
interact.modifiers.restrictSize({
|
||||||
|
min: { width: 50, height: 50 },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
inertia: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onInteraction(target) {
|
||||||
|
this.boxDragContainer.appendChild(target);
|
||||||
|
},
|
||||||
|
|
||||||
|
createDraggableCanvas() {
|
||||||
|
const createdCanvas = document.createElement('canvas');
|
||||||
|
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
|
||||||
|
createdCanvas.classList.add("draggable-canvas");
|
||||||
|
|
||||||
|
const x = 0;
|
||||||
|
const y = 20;
|
||||||
|
createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
|
createdCanvas.setAttribute('data-x', x);
|
||||||
|
createdCanvas.setAttribute('data-y', y);
|
||||||
|
|
||||||
|
createdCanvas.onclick = e => this.onInteraction(e.target);
|
||||||
|
|
||||||
|
this.boxDragContainer.appendChild(createdCanvas);
|
||||||
|
return createdCanvas;
|
||||||
|
},
|
||||||
|
createDraggableCanvasFromUrl(dataUrl) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
var myImage = new Image();
|
||||||
|
myImage.src = dataUrl;
|
||||||
|
myImage.onload = () => {
|
||||||
|
var createdCanvas = this.createDraggableCanvas();
|
||||||
|
|
||||||
|
createdCanvas.width = myImage.width;
|
||||||
|
createdCanvas.height = myImage.height;
|
||||||
|
|
||||||
|
const imgAspect = myImage.width / myImage.height;
|
||||||
|
const pdfAspect = this.boxDragContainer.offsetWidth / this.boxDragContainer.offsetHeight;
|
||||||
|
|
||||||
|
var scaleMultiplier;
|
||||||
|
if (imgAspect > pdfAspect) {
|
||||||
|
scaleMultiplier = this.boxDragContainer.offsetWidth / myImage.width;
|
||||||
|
} else {
|
||||||
|
scaleMultiplier = this.boxDragContainer.offsetHeight / myImage.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newWidth = createdCanvas.width;
|
||||||
|
var newHeight = createdCanvas.height;
|
||||||
|
if (scaleMultiplier < 1) {
|
||||||
|
newWidth = newWidth * scaleMultiplier;
|
||||||
|
newHeight = newHeight * scaleMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
createdCanvas.style.width = newWidth+"px";
|
||||||
|
createdCanvas.style.height = newHeight+"px";
|
||||||
|
|
||||||
|
var myContext = createdCanvas.getContext("2d");
|
||||||
|
myContext.drawImage(myImage,0,0);
|
||||||
|
resolve(createdCanvas);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteDraggableCanvas(element) {
|
||||||
|
element.remove();
|
||||||
|
},
|
||||||
|
deleteDraggableCanvasById(id) {
|
||||||
|
this.deleteDraggableCanvas(document.getElementById(id));
|
||||||
|
},
|
||||||
|
getLastInteracted() {
|
||||||
|
return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
|
||||||
|
},
|
||||||
|
|
||||||
|
async renderPage(pdfDocument, pageIdx) {
|
||||||
|
this.pdfDoc = pdfDocument ? pdfDocument : this.pdfDoc;
|
||||||
|
this.pageIndex = pageIdx;
|
||||||
|
const page = await this.pdfDoc.getPage(this.pageIndex+1);
|
||||||
|
|
||||||
|
// set the canvas size to the size of the page
|
||||||
|
if (page.rotate == 90 || page.rotate == 270) {
|
||||||
|
this.pdfCanvas.width = page.view[3];
|
||||||
|
this.pdfCanvas.height = page.view[2];
|
||||||
|
} else {
|
||||||
|
this.pdfCanvas.width = page.view[2];
|
||||||
|
this.pdfCanvas.height = page.view[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the page onto the canvas
|
||||||
|
var renderContext = {
|
||||||
|
canvasContext: this.pdfCanvas.getContext("2d"),
|
||||||
|
viewport: page.getViewport({ scale: 1 })
|
||||||
|
};
|
||||||
|
await page.render(renderContext).promise;
|
||||||
|
|
||||||
|
//return pdfCanvas.toDataURL();
|
||||||
|
},
|
||||||
|
async incrementPage() {
|
||||||
|
if (this.pageIndex < this.pdfDoc.numPages-1) {
|
||||||
|
return await this.renderPage(this.pdfDoc, this.pageIndex+1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async decrementPage() {
|
||||||
|
if (this.pageIndex > 0) {
|
||||||
|
return await this.renderPage(this.pdfDoc, this.pageIndex-1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getOverlayedPdfDocument() {
|
||||||
|
const pdfBytes = await this.pdfDoc.getData();
|
||||||
|
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes);
|
||||||
|
|
||||||
|
const draggables = this.boxDragContainer.querySelectorAll(".draggable-canvas");
|
||||||
|
for (const draggable of draggables) {
|
||||||
|
// embed the draggable canvas
|
||||||
|
const dataURL = draggable.toDataURL();
|
||||||
|
const response = await fetch(dataURL);
|
||||||
|
const draggableImgBytes = await response.arrayBuffer();
|
||||||
|
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
|
||||||
|
|
||||||
|
const page = pdfDocModified.getPage(this.pageIndex);
|
||||||
|
|
||||||
|
const draggablePositionPixels = this.parseTransform(draggable);
|
||||||
|
const draggablePositionRelative = {
|
||||||
|
x: draggablePositionPixels.x / this.pdfCanvas.offsetWidth,
|
||||||
|
y: draggablePositionPixels.y / this.pdfCanvas.offsetHeight,
|
||||||
|
width: draggablePositionPixels.width / this.pdfCanvas.offsetWidth,
|
||||||
|
height: draggablePositionPixels.height / this.pdfCanvas.offsetHeight,
|
||||||
|
}
|
||||||
|
const draggablePositionPdf = {
|
||||||
|
x: draggablePositionRelative.x * page.getWidth(),
|
||||||
|
y: draggablePositionRelative.y * page.getHeight(),
|
||||||
|
width: draggablePositionRelative.width * page.getWidth(),
|
||||||
|
height: draggablePositionRelative.height * page.getHeight(),
|
||||||
|
}
|
||||||
|
|
||||||
|
page.drawImage(pdfImageObject, {
|
||||||
|
x: draggablePositionPdf.x,
|
||||||
|
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
|
||||||
|
width: draggablePositionPdf.width,
|
||||||
|
height: draggablePositionPdf.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfDocModified;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
DraggableUtils.init();
|
||||||
|
});
|
3
src/main/resources/static/js/interact.min.js
vendored
Normal file
3
src/main/resources/static/js/interact.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/main/resources/static/js/signature_pad.umd.min.js
vendored
Normal file
6
src/main/resources/static/js/signature_pad.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
39
src/main/resources/static/js/tab-container.js
Normal file
39
src/main/resources/static/js/tab-container.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
TabContainer = {
|
||||||
|
initTabGroups() {
|
||||||
|
const groups = document.querySelectorAll(".tab-group");
|
||||||
|
const unloadedGroups = [...groups].filter(g => !g.initialised);
|
||||||
|
unloadedGroups.forEach(group => {
|
||||||
|
const containers = group.querySelectorAll(".tab-container");
|
||||||
|
const tabTitles = [...containers].map(c => c.getAttribute("title"));
|
||||||
|
|
||||||
|
const tabList = document.createElement("div");
|
||||||
|
tabList.classList.add("tab-buttons");
|
||||||
|
tabTitles.forEach(title => {
|
||||||
|
const tabButton = document.createElement("button");
|
||||||
|
tabButton.innerHTML = title;
|
||||||
|
tabButton.onclick = e => {
|
||||||
|
this.setActiveTab(e.target);
|
||||||
|
}
|
||||||
|
tabList.appendChild(tabButton);
|
||||||
|
});
|
||||||
|
group.prepend(tabList);
|
||||||
|
|
||||||
|
this.setActiveTab(tabList.firstChild);
|
||||||
|
|
||||||
|
group.initialised = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setActiveTab(tabButton) {
|
||||||
|
const group = tabButton.closest(".tab-group")
|
||||||
|
|
||||||
|
group.querySelectorAll(".active").forEach(el => el.classList.remove("active"));
|
||||||
|
|
||||||
|
tabButton.classList.add("active");
|
||||||
|
group.querySelector(`[title="${tabButton.innerHTML}"]`).classList.add("active");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
TabContainer.initTabGroups();
|
||||||
|
})
|
|
@ -27,8 +27,13 @@
|
||||||
|
|
||||||
<!-- Custom -->
|
<!-- Custom -->
|
||||||
<link rel="stylesheet" href="css/general.css">
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" th:href="@{css/light-mode.css}" id="light-mode-styles">
|
||||||
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
|
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
|
||||||
<link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true">
|
<link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true">
|
||||||
|
<link rel="stylesheet" href="css/tab-container.css">
|
||||||
|
<script src="js/tab-container.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var toggleCount = 0;
|
var toggleCount = 0;
|
||||||
var lastToggleTime = Date.now();
|
var lastToggleTime = Date.now();
|
||||||
|
@ -42,22 +47,26 @@ function toggleDarkMode() {
|
||||||
}
|
}
|
||||||
lastToggleTime = currentTime;
|
lastToggleTime = currentTime;
|
||||||
|
|
||||||
|
var lightModeStyles = document.getElementById("light-mode-styles");
|
||||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||||
var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
||||||
var darkModeIcon = document.getElementById("dark-mode-icon");
|
var darkModeIcon = document.getElementById("dark-mode-icon");
|
||||||
|
|
||||||
if (toggleCount >= 18) {
|
if (toggleCount >= 18) {
|
||||||
localStorage.setItem("dark-mode", "rainbow");
|
localStorage.setItem("dark-mode", "rainbow");
|
||||||
|
lightModeStyles.disabled = true;
|
||||||
darkModeStyles.disabled = true;
|
darkModeStyles.disabled = true;
|
||||||
rainbowModeStyles.disabled = false;
|
rainbowModeStyles.disabled = false;
|
||||||
darkModeIcon.src = "rainbow.svg";
|
darkModeIcon.src = "rainbow.svg";
|
||||||
} else if (localStorage.getItem("dark-mode") == "on") {
|
} else if (localStorage.getItem("dark-mode") == "on") {
|
||||||
localStorage.setItem("dark-mode", "off");
|
localStorage.setItem("dark-mode", "off");
|
||||||
|
lightModeStyles.disabled = false;
|
||||||
darkModeStyles.disabled = true;
|
darkModeStyles.disabled = true;
|
||||||
rainbowModeStyles.disabled = true;
|
rainbowModeStyles.disabled = true;
|
||||||
darkModeIcon.src = "sun.svg";
|
darkModeIcon.src = "sun.svg";
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem("dark-mode", "on");
|
localStorage.setItem("dark-mode", "on");
|
||||||
|
lightModeStyles.disabled = true;
|
||||||
darkModeStyles.disabled = false;
|
darkModeStyles.disabled = false;
|
||||||
rainbowModeStyles.disabled = true;
|
rainbowModeStyles.disabled = true;
|
||||||
darkModeIcon.src = "moon.svg";
|
darkModeIcon.src = "moon.svg";
|
||||||
|
@ -65,19 +74,23 @@ function toggleDarkMode() {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
var lightModeStyles = document.getElementById("light-mode-styles");
|
||||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||||
var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
||||||
var darkModeIcon = document.getElementById("dark-mode-icon");
|
var darkModeIcon = document.getElementById("dark-mode-icon");
|
||||||
|
|
||||||
if (localStorage.getItem("dark-mode") == "on") {
|
if (localStorage.getItem("dark-mode") == "on") {
|
||||||
|
lightModeStyles.disabled = true;
|
||||||
darkModeStyles.disabled = false;
|
darkModeStyles.disabled = false;
|
||||||
rainbowModeStyles.disabled = true;
|
rainbowModeStyles.disabled = true;
|
||||||
darkModeIcon.src = "moon.svg";
|
darkModeIcon.src = "moon.svg";
|
||||||
} else if (localStorage.getItem("dark-mode") == "off") {
|
} else if (localStorage.getItem("dark-mode") == "off") {
|
||||||
|
lightModeStyles.disabled = false;
|
||||||
darkModeStyles.disabled = true;
|
darkModeStyles.disabled = true;
|
||||||
rainbowModeStyles.disabled = true;
|
rainbowModeStyles.disabled = true;
|
||||||
darkModeIcon.src = "sun.svg";
|
darkModeIcon.src = "sun.svg";
|
||||||
} else if (localStorage.getItem("dark-mode") == "rainbow") {
|
} else if (localStorage.getItem("dark-mode") == "rainbow") {
|
||||||
|
lightModeStyles.disabled = true;
|
||||||
darkModeStyles.disabled = true;
|
darkModeStyles.disabled = true;
|
||||||
rainbowModeStyles.disabled = false;
|
rainbowModeStyles.disabled = false;
|
||||||
darkModeIcon.src = "rainbow.svg";
|
darkModeIcon.src = "rainbow.svg";
|
||||||
|
@ -478,7 +491,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
</div>
|
</div>
|
||||||
<div class="selected-files"></div>
|
<div class="selected-files"></div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
<div id="progressBarContainer" style="display: none; position: relative;">
|
<div id="progressBarContainer" style="display: none; position: relative;">
|
||||||
<div class="progress" style="height: 1rem;">
|
<div class="progress" style="height: 1rem;">
|
||||||
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
background: rgba(13, 110, 253, 0.1);
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
border: 1px solid rgba(0, 0, 0, .25);
|
border: 1px solid rgba(0, 0, 0, .25);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
|
|
||||||
|
|
135
src/main/resources/templates/other/compare.html
Normal file
135
src/main/resources/templates/other/compare.html
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{repair.title})}"></th:block>
|
||||||
|
|
||||||
|
|
||||||
|
<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="#{repair.header}"></h2>
|
||||||
|
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput2', multiple=false, accept='application/pdf')}"></div>
|
||||||
|
<button onclick="comparePDFs()">Compare</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
<script>
|
||||||
|
async function comparePDFs() {
|
||||||
|
const file1 = document.getElementById("fileInput-input").files[0];
|
||||||
|
const file2 = document.getElementById("fileInput2-input").files[0];
|
||||||
|
|
||||||
|
if (!file1 || !file2) {
|
||||||
|
console.error("Please select two PDF files to compare");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [pdf1, pdf2] = await Promise.all([
|
||||||
|
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
|
||||||
|
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
|
||||||
|
]);
|
||||||
|
|
||||||
|
const extractText = async (pdf) => {
|
||||||
|
const pages = [];
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const content = await page.getTextContent();
|
||||||
|
const strings = content.items.map(item => item.str);
|
||||||
|
pages.push(strings.join(""));
|
||||||
|
}
|
||||||
|
return pages.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const [text1, text2] = await Promise.all([
|
||||||
|
extractText(pdf1),
|
||||||
|
extractText(pdf2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const diff = (text1, text2) => {
|
||||||
|
const lines1 = text1.split("\n");
|
||||||
|
const lines2 = text2.split("\n");
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
let i = 0, j = 0;
|
||||||
|
|
||||||
|
while (i < lines1.length || j < lines2.length) {
|
||||||
|
console.log(`lines1[${i}]='${lines1[i]}', lines2[${j}]='${lines2[j]}'`);
|
||||||
|
console.log(`i=${i}, j=${j}`);
|
||||||
|
if (lines1[i] === lines2[j]) {
|
||||||
|
result.push([i, j, lines1[i]]);
|
||||||
|
i++;
|
||||||
|
j++;
|
||||||
|
console.log(`i=${i}, j=${j}`);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let k = i, l = j;
|
||||||
|
while (k < lines1.length && l < lines2.length && lines1[k] !== lines2[l]) {
|
||||||
|
k++;
|
||||||
|
l++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = i; x < k; x++) {
|
||||||
|
result.push([x, -1, lines1[x]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = j; y < l; y++) {
|
||||||
|
result.push([-1, y, lines2[y]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
i = k;
|
||||||
|
j = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const differences = diff(text1, text2);
|
||||||
|
const highlightDifferences = async (pdf, differences) => {
|
||||||
|
for (const difference of differences) {
|
||||||
|
const [pageIndex, lineIndex, lineText] = difference;
|
||||||
|
if (lineIndex === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log(pageIndex);
|
||||||
|
const page = await pdf.getPage(pageIndex);
|
||||||
|
const viewport = page.getViewport({ scale: 1 });
|
||||||
|
const [left,top] = viewport.convertToViewportPoint(0, lineIndex * 20);
|
||||||
|
const [right, bottom] = viewport.convertToViewportPoint(500, (lineIndex + 1) * 20);
|
||||||
|
const annotation = {
|
||||||
|
type: "Highlight",
|
||||||
|
rect: [left, top, right - left, bottom - top],
|
||||||
|
color: [255, 255, 0],
|
||||||
|
opacity: 0.5,
|
||||||
|
quadPoints:
|
||||||
|
[
|
||||||
|
left, top, right, top, right, bottom, left, bottom
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.addAnnotation(annotation);
|
||||||
|
|
||||||
|
const message = `Difference found in page ${pageIndex }, line ${lineIndex + 1}: ${lineText}`;
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.textContent = message;
|
||||||
|
document.getElementById("result").appendChild(p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await highlightDifferences(pdf1, differences);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -3,70 +3,21 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{sign.title})}"></th:block>
|
<th:block th:insert="~{fragments/common :: head(title=#{sign.title})}"></th:block>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<script src="js/signature_pad.umd.min.js"></script>
|
||||||
<title>PDF Signature App</title>
|
<script src="js/interact.min.js"></script>
|
||||||
<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>
|
|
||||||
<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">
|
|
||||||
|
|
||||||
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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%;
|
|
||||||
}
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Estonia';
|
||||||
|
src: url(fonts/Estonia.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Tangerine';
|
||||||
|
src: url(fonts/Tangerine.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<th:block th:insert="~{fragments/common.html :: game}"></th:block>
|
|
||||||
<div id="page-container">
|
<div id="page-container">
|
||||||
<div id="content-wrap">
|
<div id="content-wrap">
|
||||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||||
|
@ -75,316 +26,264 @@
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2 th:text="#{sign.header}"></h2>
|
<h2 th:text="#{sign.header}"></h2>
|
||||||
|
|
||||||
|
<!-- pdf selector -->
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
|
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
|
||||||
|
<script>
|
||||||
|
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const pdfData = await file.arrayBuffer();
|
||||||
|
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||||
|
|
||||||
<div class = "btn-group">
|
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||||
<input type="radio" class="btn-check" name="signature-type" id="draw-signature" autocomplete="off" checked>
|
el.style.cssText = '';
|
||||||
<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">
|
}
|
||||||
<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">
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
<label class="btn btn-outline-secondary" for="type-signature">Type</label>
|
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="#{sign.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="#{sign.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);
|
||||||
|
|
||||||
|
// This part causes the canvas to be cleared
|
||||||
|
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio;
|
||||||
|
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio;
|
||||||
|
signaturePadCanvas.getContext("2d").scale(ratio, ratio);
|
||||||
|
|
||||||
|
// 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="#{sign.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 value="Estonia" class="estonia-font">Estonia</option>
|
||||||
|
<option value="Tangerine" class="tangerine-font">Tangerine</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>
|
||||||
|
<style>
|
||||||
|
#font-select option {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='signature-upload', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div>
|
<!-- draggables box -->
|
||||||
<!-- Signature Pad -->
|
<div id="box-drag-container" class="show-on-file-selected">
|
||||||
<div id="signature-pad-container">
|
|
||||||
<canvas id="signature-pad-canvas"></canvas>
|
|
||||||
<br>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
<canvas id="pdf-canvas"></canvas>
|
||||||
<canvas id="signature-canvas" hidden style="position: absolute;" data-scale="1"></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;
|
||||||
|
}
|
||||||
|
#pdf-canvas {
|
||||||
|
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
|
||||||
|
margin: 20px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.draggable-buttons-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
<button id="download-pdf" class="btn btn-primary mb-2">Download PDF</button>
|
<!-- 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 = 'signed-document.pdf';
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
|
|
||||||
const fontSelect = document.getElementById('font-select');
|
|
||||||
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');
|
|
||||||
const signatureTypeContainer = document.getElementById('signature-type-container');
|
|
||||||
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = "none"
|
|
||||||
signatureTypeContainer.style.display = 'none';
|
|
||||||
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
|
||||||
minWidth: 1,
|
|
||||||
maxWidth: 2,
|
|
||||||
penColor: 'black',
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const pdfCtx = pdfCanvas.getContext('2d');
|
|
||||||
let pdfDoc = null;
|
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js';
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clearSignatureBtn.addEventListener('click', () => {
|
|
||||||
signaturePad.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[name=signature-type]").change(function() {
|
|
||||||
const drawSignatureInput = document.getElementById('draw-signature');
|
|
||||||
const importImageInput = document.getElementById('import-image');
|
|
||||||
const typeSignatureInput = document.getElementById('type-signature');
|
|
||||||
|
|
||||||
signaturePadContainer.style.display = drawSignatureInput.checked ? 'block' : 'none';
|
|
||||||
signatureTypeContainer.style.display = typeSignatureInput.checked ? 'block' : 'none';
|
|
||||||
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = importImageInput.checked ? 'block' : 'none';
|
|
||||||
|
|
||||||
if (drawSignatureInput.checked) {
|
|
||||||
populateSignatureFromPad();
|
|
||||||
} else if (importImageInput.checked) {
|
|
||||||
populateSignatureFromFileUpload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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)`;
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
img.src = imgUrl;
|
|
||||||
}
|
|
||||||
function populateSignatureFromFileUpload() {
|
|
||||||
const file = signatureUpload.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => populateSignature(e.target.result);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
function populateSignatureFromPad() {
|
|
||||||
if (!document.getElementById('draw-signature').checked) return;
|
|
||||||
if (signaturePad.isEmpty()) return;
|
|
||||||
|
|
||||||
const dataURL = signaturePad.toDataURL();
|
|
||||||
populateSignature(dataURL);
|
|
||||||
}
|
|
||||||
signatureUpload.addEventListener('change', populateSignatureFromFileUpload);
|
|
||||||
saveSignatureBtn.addEventListener('click', populateSignatureFromPad);
|
|
||||||
|
|
||||||
|
|
||||||
function renderPage(pageNum) {
|
|
||||||
pdfDoc.getPage(pageNum).then((page) => {
|
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
|
||||||
pdfCanvas.width = viewport.width;
|
|
||||||
pdfCanvas.height = viewport.height;
|
|
||||||
|
|
||||||
const renderCtx = {
|
|
||||||
canvasContext: pdfCtx,
|
|
||||||
viewport: viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
page.render(renderCtx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
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) => {
|
|
||||||
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
|
|
||||||
|
|
||||||
target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
async function getSignatureImage() {
|
|
||||||
const dataURL = signatureCanvas.toDataURL();
|
|
||||||
return dataURLToArrayBuffer(dataURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function dataURLToArrayBuffer(dataURL) {
|
|
||||||
const response = await fetch(dataURL);
|
|
||||||
return response.arrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue