diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 0f3580d5..2725e5f5 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 💬 Discord Server
- url: https://discord.gg/Cn8pWhQRxZ
+ url: https://discord.gg/HYmhKj45pU
about: You can join our Discord server for real time discussion and support
diff --git a/README.md b/README.md
index e8ad34cb..a1fe522c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Stirling-PDF
[](https://hub.docker.com/r/frooodle/s-pdf)
-[](https://discord.gg/Cn8pWhQRxZ)
+[](https://discord.gg/HYmhKj45pU)
[](https://github.com/Stirling-Tools/Stirling-PDF/)
[](https://github.com/Stirling-Tools/stirling-pdf)
diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java
index d738ae79..be6b5019 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java
@@ -39,10 +39,7 @@ public class PasswordController {
}
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
- @Operation(
- summary = "Remove password from a PDF file",
- description =
- "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
+ @Operation(summary = "Remove password from a PDF file", description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput();
@@ -52,15 +49,12 @@ public class PasswordController {
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
- .replaceFirst("[.][^.]+$", "")
+ .replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf");
}
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
- @Operation(
- summary = "Add password to a PDF file",
- description =
- "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
+ @Operation(summary = "Add password to a PDF file", description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput();
@@ -98,12 +92,12 @@ public class PasswordController {
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
- .replaceFirst("[.][^.]+$", "")
+ .replaceFirst("[.][^.]+$", "")
+ "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
- .replaceFirst("[.][^.]+$", "")
+ .replaceFirst("[.][^.]+$", "")
+ "_passworded.pdf");
}
}
diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties
index 0f7bdd1c..4056f972 100644
--- a/src/main/resources/messages_en_GB.properties
+++ b/src/main/resources/messages_en_GB.properties
@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
+#decrypt
+decrypt.passwordPrompt=This file is password-protected. Please enter the password:
+decrypt.cancelled=Operation cancelled for PDF: {0}
+decrypt.noPassword=No password provided for encrypted PDF: {0}
+decrypt.invalidPassword=Please try again with the correct password.
+decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
+decrypt.unexpectedError=There was an error processing the file. Please try again.
+decrypt.serverError=Server error while decrypting: {0}
+decrypt.success=File decrypted successfully.
+
#multiTool-advert
multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features!
diff --git a/src/main/resources/static/js/DecryptFiles.js b/src/main/resources/static/js/DecryptFiles.js
new file mode 100644
index 00000000..b2dbcac4
--- /dev/null
+++ b/src/main/resources/static/js/DecryptFiles.js
@@ -0,0 +1,116 @@
+export class DecryptFile {
+ async decryptFile(file, requiresPassword) {
+ try {
+ const formData = new FormData();
+ formData.append('fileInput', file);
+ if (requiresPassword) {
+ const password = prompt(`${window.decrypt.passwordPrompt}`);
+
+ if (password === null) {
+ // User cancelled
+ console.error(`Password prompt cancelled for PDF: ${file.name}`);
+ return null; // No file to return
+ }
+
+ if (!password) {
+ // No password provided
+ console.error(`No password provided for encrypted PDF: ${file.name}`);
+ this.showErrorBanner(
+ `${window.decrypt.noPassword.replace('{0}', file.name)}`,
+ '',
+ `${window.decrypt.unexpectedError}`
+ );
+ return null; // No file to return
+ }
+
+ formData.append('password', password);
+ }
+ // Send decryption request
+ const response = await fetch('/api/v1/security/remove-password', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (response.ok) {
+ const decryptedBlob = await response.blob();
+ this.removeErrorBanner();
+ return new File([decryptedBlob], file.name, {type: 'application/pdf'});
+ } else {
+ const errorText = await response.text();
+ console.error(`${window.decrypt.invalidPassword} ${errorText}`);
+ this.showErrorBanner(
+ `${window.decrypt.invalidPassword}`,
+ errorText,
+ `${window.decrypt.invalidPasswordHeader.replace('{0}', file.name)}`
+ );
+ return null; // No file to return
+ }
+ } catch (error) {
+ // Handle network or unexpected errors
+ console.error(`Failed to decrypt PDF: ${file.name}`, error);
+ this.showErrorBanner(
+ `${window.decrypt.unexpectedError.replace('{0}', file.name)}`,
+ `${error.message || window.decrypt.unexpectedError}`,
+ error
+ );
+ return null; // No file to return
+ }
+ }
+
+ async checkFileEncrypted(file) {
+ try {
+ if (file.type !== 'application/pdf') {
+ return {isEncrypted: false, requiresPassword: false};
+ }
+
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ const arrayBuffer = await file.arrayBuffer();
+ const arrayBufferForPdfLib = arrayBuffer.slice(0);
+
+ const loadingTask = pdfjsLib.getDocument({
+ data: arrayBuffer,
+ });
+
+ await loadingTask.promise;
+
+ try {
+ //Uses PDFLib.PDFDocument to check if unpassworded but encrypted
+ const pdfDoc = await PDFLib.PDFDocument.load(arrayBufferForPdfLib);
+ return {isEncrypted: false, requiresPassword: false};
+ } catch (error) {
+ if (error.message.includes('Input document to `PDFDocument.load` is encrypted')) {
+ return {isEncrypted: true, requiresPassword: false};
+ }
+ console.error('Error checking encryption:', error);
+ throw new Error('Failed to determine if the file is encrypted.');
+ }
+ } catch (error) {
+ if (error.name === 'PasswordException') {
+ if (error.code === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
+ return {isEncrypted: true, requiresPassword: true};
+ } else if (error.code === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
+ return {isEncrypted: true, requiresPassword: false};
+ }
+ }
+
+ console.error('Error checking encryption:', error);
+ throw new Error('Failed to determine if the file is encrypted.');
+ }
+ }
+
+ showErrorBanner(message, stackTrace, error) {
+ const errorContainer = document.getElementById('errorContainer');
+ errorContainer.style.display = 'block'; // Display the banner
+ errorContainer.querySelector('.alert-heading').textContent = error;
+ errorContainer.querySelector('p').textContent = message;
+ document.querySelector('#traceContent').textContent = stackTrace;
+ }
+
+ removeErrorBanner() {
+ const errorContainer = document.getElementById('errorContainer');
+ errorContainer.style.display = 'none'; // Hide the banner
+ errorContainer.querySelector('.alert-heading').textContent = '';
+ errorContainer.querySelector('p').textContent = '';
+ document.querySelector('#traceContent').textContent = '';
+ }
+}
diff --git a/src/main/resources/static/js/download.js b/src/main/resources/static/js/download.js
new file mode 100644
index 00000000..8eed99f9
--- /dev/null
+++ b/src/main/resources/static/js/download.js
@@ -0,0 +1,27 @@
+document.getElementById('download-pdf').addEventListener('click', async () => {
+ const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
+ let decryptedFile = modifiedPdf;
+ let isEncrypted = false;
+ let requiresPassword = false;
+ await this.decryptFile
+ .checkFileEncrypted(decryptedFile)
+ .then((result) => {
+ isEncrypted = result.isEncrypted;
+ requiresPassword = result.requiresPassword;
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ if (decryptedFile.type === 'application/pdf' && isEncrypted) {
+ decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
+ if (!decryptedFile) {
+ throw new Error('File decryption failed.');
+ }
+ }
+ 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();
+});
diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js
index 4131b49a..f89c28ec 100644
--- a/src/main/resources/static/js/downloader.js
+++ b/src/main/resources/static/js/downloader.js
@@ -42,7 +42,7 @@
event.preventDefault();
firstErrorOccurred = false;
const url = this.action;
- const files = $('#fileInput-input')[0].files;
+ let files = $('#fileInput-input')[0].files;
const formData = new FormData(this);
const submitButton = document.getElementById('submitBtn');
const showGameBtn = document.getElementById('show-game-btn');
@@ -71,6 +71,16 @@
}, 5000);
try {
+ if (!url.includes('remove-password')) {
+ // Check if any PDF files are encrypted and handle decryption if necessary
+ const decryptedFiles = await checkAndDecryptFiles(url, files);
+ files = decryptedFiles;
+ // Append decrypted files to formData
+ decryptedFiles.forEach((file, index) => {
+ formData.set(`fileInput`, file);
+ });
+ }
+
submitButton.textContent = 'Processing...';
submitButton.disabled = true;
@@ -133,6 +143,98 @@
}
}
+ async function checkAndDecryptFiles(url, files) {
+ const decryptedFiles = [];
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+
+ // Extract the base URL
+ const baseUrl = new URL(url);
+ let removePasswordUrl = `${baseUrl.origin}`;
+
+ // Check if there's a path before /api/
+ const apiIndex = baseUrl.pathname.indexOf('/api/');
+ if (apiIndex > 0) {
+ removePasswordUrl += baseUrl.pathname.substring(0, apiIndex);
+ }
+
+ // Append the new endpoint
+ removePasswordUrl += '/api/v1/security/remove-password';
+
+ console.log(`Remove password URL: ${removePasswordUrl}`);
+
+ for (const file of files) {
+ console.log(`Processing file: ${file.name}`);
+ if (file.type !== 'application/pdf') {
+ console.log(`Skipping non-PDF file: ${file.name}`);
+ decryptedFiles.push(file);
+ continue;
+ }
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const loadingTask = pdfjsLib.getDocument({data: arrayBuffer});
+
+ console.log(`Attempting to load PDF: ${file.name}`);
+ const pdf = await loadingTask.promise;
+ console.log(`File is not encrypted: ${file.name}`);
+ decryptedFiles.push(file); // If no error, file is not encrypted
+ } catch (error) {
+ if (error.name === 'PasswordException' && error.code === 1) {
+ console.log(`PDF requires password: ${file.name}`, error);
+ console.log(`Attempting to remove password from PDF: ${file.name} with password.`);
+ const password = prompt(`${window.translations.decrypt.passwordPrompt}`);
+
+ if (!password) {
+ console.error(`No password provided for encrypted PDF: ${file.name}`);
+ showErrorBanner(
+ `${window.translations.decrypt.noPassword.replace('{0}', file.name)}`,
+ `${window.translations.decrypt.unexpectedError}`
+ );
+ throw error;
+ }
+
+ try {
+ // Prepare FormData for the decryption request
+ const formData = new FormData();
+ formData.append('fileInput', file);
+ formData.append('password', password);
+
+ // Use handleSingleDownload to send the request
+ const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
+
+ if (decryptionResult && decryptionResult.blob) {
+ const decryptedBlob = await decryptionResult.blob();
+ const decryptedFile = new File([decryptedBlob], file.name, {type: 'application/pdf'});
+
+ /* // Create a link element to download the file
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(decryptedBlob);
+ link.download = 'test.pdf';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+*/
+ decryptedFiles.push(decryptedFile);
+ console.log(`Successfully decrypted PDF: ${file.name}`);
+ } else {
+ throw new Error('Decryption failed: No valid response from server');
+ }
+ } catch (decryptError) {
+ console.error(`Failed to decrypt PDF: ${file.name}`, decryptError);
+ showErrorBanner(
+ `${window.translations.invalidPasswordHeader.replace('{0}', file.name)}`,
+ `${window.translations.invalidPassword}`
+ );
+ throw decryptError;
+ }
+ } else {
+ console.log(`Error loading PDF: ${file.name}`, error);
+ throw error;
+ }
+ }
+ }
+ return decryptedFiles;
+ }
+
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
const startTime = performance.now();
const file = formData.get('fileInput');
diff --git a/src/main/resources/static/js/draggable-utils.js b/src/main/resources/static/js/draggable-utils.js
index bdd75a44..aa0c15a0 100644
--- a/src/main/resources/static/js/draggable-utils.js
+++ b/src/main/resources/static/js/draggable-utils.js
@@ -1,6 +1,6 @@
const DraggableUtils = {
- boxDragContainer: document.getElementById("box-drag-container"),
- pdfCanvas: document.getElementById("pdf-canvas"),
+ boxDragContainer: document.getElementById('box-drag-container'),
+ pdfCanvas: document.getElementById('pdf-canvas'),
nextId: 0,
pdfDoc: null,
pageIndex: 0,
@@ -9,19 +9,17 @@ const DraggableUtils = {
lastInteracted: null,
init() {
- interact(".draggable-canvas")
+ 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;
+ 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);
+ target.setAttribute('data-bs-x', x);
+ target.setAttribute('data-bs-y', y);
this.onInteraction(target);
//update the last interacted element
@@ -30,12 +28,12 @@ const DraggableUtils = {
},
})
.resizable({
- edges: { left: true, right: true, bottom: true, top: true },
+ 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;
+ 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) {
@@ -44,8 +42,7 @@ const DraggableUtils = {
let width = event.rect.width;
let height = event.rect.height;
- if (Math.abs(event.deltaRect.width) >= Math.abs(
- event.deltaRect.height)) {
+ if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
height = width / aspectRatio;
} else {
width = height * aspectRatio;
@@ -55,19 +52,18 @@ const DraggableUtils = {
event.rect.height = height;
}
- target.style.width = event.rect.width + "px";
- target.style.height = event.rect.height + "px";
+ 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.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);
+ 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);
},
@@ -75,7 +71,7 @@ const DraggableUtils = {
modifiers: [
interact.modifiers.restrictSize({
- min: { width: 5, height: 5 },
+ min: {width: 5, height: 5},
}),
],
inertia: true,
@@ -95,8 +91,8 @@ const DraggableUtils = {
const stepY = target.offsetHeight * 0.05;
// Get the current x and y coordinates
- let x = (parseFloat(target.getAttribute('data-bs-x')) || 0);
- let y = (parseFloat(target.getAttribute('data-bs-y')) || 0);
+ let x = parseFloat(target.getAttribute('data-bs-x')) || 0;
+ let y = parseFloat(target.getAttribute('data-bs-y')) || 0;
// Check which key was pressed and update the coordinates accordingly
switch (event.key) {
@@ -135,15 +131,15 @@ const DraggableUtils = {
},
createDraggableCanvas() {
- const createdCanvas = document.createElement("canvas");
+ const createdCanvas = document.createElement('canvas');
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
- createdCanvas.classList.add("draggable-canvas");
+ createdCanvas.classList.add('draggable-canvas');
const x = 0;
const y = 20;
createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
- createdCanvas.setAttribute("data-bs-x", x);
- createdCanvas.setAttribute("data-bs-y", y);
+ createdCanvas.setAttribute('data-bs-x', x);
+ createdCanvas.setAttribute('data-bs-y', y);
//Click element in order to enable arrow keys
createdCanvas.addEventListener('click', () => {
@@ -186,29 +182,29 @@ const DraggableUtils = {
newHeight = newHeight * scaleMultiplier;
}
- createdCanvas.style.width = newWidth + "px";
- createdCanvas.style.height = newHeight + "px";
+ createdCanvas.style.width = newWidth + 'px';
+ createdCanvas.style.height = newHeight + 'px';
- var myContext = createdCanvas.getContext("2d");
+ var myContext = createdCanvas.getContext('2d');
myContext.drawImage(myImage, 0, 0);
resolve(createdCanvas);
};
});
},
deleteAllDraggableCanvases() {
- this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach((el) => el.remove());
+ this.boxDragContainer.querySelectorAll('.draggable-canvas').forEach((el) => el.remove());
},
async addAllPagesDraggableCanvas(element) {
if (element) {
- let currentPage = this.pageIndex
+ let currentPage = this.pageIndex;
if (!this.elementAllPages.includes(element)) {
- this.elementAllPages.push(element)
+ this.elementAllPages.push(element);
element.style.filter = 'sepia(1) hue-rotate(90deg) brightness(1.2)';
let newElement = {
- "element": element,
- "offsetWidth": element.width,
- "offsetHeight": element.height
- }
+ element: element,
+ offsetWidth: element.width,
+ offsetHeight: element.height,
+ };
let pagesMap = this.documentsMap.get(this.pdfDoc);
@@ -216,21 +212,20 @@ const DraggableUtils = {
pagesMap = {};
this.documentsMap.set(this.pdfDoc, pagesMap);
}
- let page = this.pageIndex
+ let page = this.pageIndex;
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
-
if (pagesMap[`${pageIndex}-offsetWidth`]) {
if (!pagesMap[pageIndex].includes(newElement)) {
pagesMap[pageIndex].push(newElement);
}
} else {
- pagesMap[pageIndex] = []
- pagesMap[pageIndex].push(newElement)
+ pagesMap[pageIndex] = [];
+ pagesMap[pageIndex].push(newElement);
pagesMap[`${pageIndex}-offsetWidth`] = pagesMap[`${page}-offsetWidth`];
pagesMap[`${pageIndex}-offsetHeight`] = pagesMap[`${page}-offsetHeight`];
}
- await this.goToPage(pageIndex)
+ await this.goToPage(pageIndex);
}
} else {
const index = this.elementAllPages.indexOf(element);
@@ -247,17 +242,17 @@ const DraggableUtils = {
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
if (pagesMap[`${pageIndex}-offsetWidth`] && pageIndex != currentPage) {
const pageElements = pagesMap[pageIndex];
- pageElements.forEach(elementPage => {
- const elementIndex = pageElements.findIndex(elementPage => elementPage['element'].id === element.id);
+ pageElements.forEach((elementPage) => {
+ const elementIndex = pageElements.findIndex((elementPage) => elementPage['element'].id === element.id);
if (elementIndex !== -1) {
pageElements.splice(elementIndex, 1);
}
});
}
- await this.goToPage(pageIndex)
+ await this.goToPage(pageIndex);
}
}
- await this.goToPage(currentPage)
+ await this.goToPage(currentPage);
}
},
deleteDraggableCanvas(element) {
@@ -271,7 +266,7 @@ const DraggableUtils = {
}
},
getLastInteracted() {
- return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
+ return this.boxDragContainer.querySelector('.draggable-canvas:last-of-type');
},
storePageContents() {
@@ -280,7 +275,7 @@ const DraggableUtils = {
pagesMap = {};
}
- const elements = [...this.boxDragContainer.querySelectorAll(".draggable-canvas")];
+ const elements = [...this.boxDragContainer.querySelectorAll('.draggable-canvas')];
const draggablesData = elements.map((el) => {
return {
element: el,
@@ -291,8 +286,8 @@ const DraggableUtils = {
elements.forEach((el) => this.boxDragContainer.removeChild(el));
pagesMap[this.pageIndex] = draggablesData;
- pagesMap[this.pageIndex + "-offsetWidth"] = this.pdfCanvas.offsetWidth;
- pagesMap[this.pageIndex + "-offsetHeight"] = this.pdfCanvas.offsetHeight;
+ pagesMap[this.pageIndex + '-offsetWidth'] = this.pdfCanvas.offsetWidth;
+ pagesMap[this.pageIndex + '-offsetHeight'] = this.pdfCanvas.offsetHeight;
this.documentsMap.set(this.pdfDoc, pagesMap);
},
@@ -329,8 +324,8 @@ const DraggableUtils = {
// render the page onto the canvas
var renderContext = {
- canvasContext: this.pdfCanvas.getContext("2d"),
- viewport: page.getViewport({ scale: 1 }),
+ canvasContext: this.pdfCanvas.getContext('2d'),
+ viewport: page.getViewport({scale: 1}),
};
await page.render(renderContext).promise;
@@ -358,7 +353,7 @@ const DraggableUtils = {
}
},
- parseTransform(element) { },
+ parseTransform(element) {},
async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
@@ -369,7 +364,7 @@ const DraggableUtils = {
const pagesMap = this.documentsMap.get(this.pdfDoc);
for (let pageIdx in pagesMap) {
- if (pageIdx.includes("offset")) {
+ if (pageIdx.includes('offset')) {
continue;
}
console.log(typeof pageIdx);
@@ -377,9 +372,8 @@ const DraggableUtils = {
const page = pdfDocModified.getPage(parseInt(pageIdx));
let draggablesData = pagesMap[pageIdx];
- const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
- const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
-
+ const offsetWidth = pagesMap[pageIdx + '-offsetWidth'];
+ const offsetHeight = pagesMap[pageIdx + '-offsetHeight'];
for (const draggableData of draggablesData) {
// embed the draggable canvas
@@ -389,8 +383,8 @@ const DraggableUtils = {
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 tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
+ const transformComponents = tansform.split(',');
const draggablePositionPixels = {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
@@ -429,9 +423,8 @@ const DraggableUtils = {
};
//Defining the image if the page has a 0-degree angle
- let x = draggablePositionPdf.x
- let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height
-
+ let x = draggablePositionPdf.x;
+ let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
//Defining the image position if it is at other angles
if (normalizedAngle === 90) {
@@ -451,7 +444,7 @@ const DraggableUtils = {
y: y,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
- rotate: rotation
+ rotate: rotation,
});
}
}
@@ -460,6 +453,6 @@ const DraggableUtils = {
},
};
-document.addEventListener("DOMContentLoaded", () => {
+document.addEventListener('DOMContentLoaded', () => {
DraggableUtils.init();
});
diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js
index e288f5b8..63485c60 100644
--- a/src/main/resources/static/js/fileInput.js
+++ b/src/main/resources/static/js/fileInput.js
@@ -1,20 +1,19 @@
-import FileIconFactory from "./file-icon-factory.js";
-import FileUtils from "./file-utils.js";
+import FileIconFactory from './file-icon-factory.js';
+import FileUtils from './file-utils.js';
import UUID from './uuid.js';
-
+import {DecryptFile} from './DecryptFiles.js';
let isScriptExecuted = false;
if (!isScriptExecuted) {
isScriptExecuted = true;
- document.addEventListener("DOMContentLoaded", function () {
- document.querySelectorAll(".custom-file-chooser").forEach(setupFileInput);
+ document.addEventListener('DOMContentLoaded', function () {
+ document.querySelectorAll('.custom-file-chooser').forEach(setupFileInput);
});
}
-
function setupFileInput(chooser) {
- const elementId = chooser.getAttribute("data-bs-element-id");
- const filesSelected = chooser.getAttribute("data-bs-files-selected");
- const pdfPrompt = chooser.getAttribute("data-bs-pdf-prompt");
+ const elementId = chooser.getAttribute('data-bs-element-id');
+ const filesSelected = chooser.getAttribute('data-bs-files-selected');
+ const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt');
const inputContainerId = chooser.getAttribute('data-bs-element-container-id');
let inputContainer = document.getElementById(inputContainerId);
@@ -26,7 +25,7 @@ function setupFileInput(chooser) {
inputContainer.addEventListener('click', (e) => {
let inputBtn = document.getElementById(elementId);
inputBtn.click();
- })
+ });
const dragenterListener = function () {
dragCounter++;
@@ -63,7 +62,7 @@ function setupFileInput(chooser) {
const files = dt.files;
const fileInput = document.getElementById(elementId);
- if (fileInput?.hasAttribute("multiple")) {
+ if (fileInput?.hasAttribute('multiple')) {
pushFileListTo(files, allFiles);
} else if (fileInput) {
allFiles = [files[0]];
@@ -78,7 +77,7 @@ function setupFileInput(chooser) {
dragCounter = 0;
- fileInput.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: {source: 'drag-drop'} }));
+ fileInput.dispatchEvent(new CustomEvent('change', {bubbles: true, detail: {source: 'drag-drop'}}));
};
function pushFileListTo(fileList, container) {
@@ -87,7 +86,7 @@ function setupFileInput(chooser) {
}
}
- ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false);
});
@@ -96,37 +95,50 @@ function setupFileInput(chooser) {
e.stopPropagation();
}
- document.body.addEventListener("dragenter", dragenterListener);
- document.body.addEventListener("dragleave", dragleaveListener);
- document.body.addEventListener("drop", dropListener);
+ document.body.addEventListener('dragenter', dragenterListener);
+ document.body.addEventListener('dragleave', dragleaveListener);
+ document.body.addEventListener('drop', dropListener);
- $("#" + elementId).on("change", function (e) {
+ $('#' + elementId).on('change', async function (e) {
let element = e.target;
const isDragAndDrop = e.detail?.source == 'drag-drop';
- if (element instanceof HTMLInputElement && element.hasAttribute("multiple")) {
- allFiles = isDragAndDrop ? allFiles : [... allFiles, ... element.files];
+ if (element instanceof HTMLInputElement && element.hasAttribute('multiple')) {
+ allFiles = isDragAndDrop ? allFiles : [...allFiles, ...element.files];
} else {
allFiles = Array.from(isDragAndDrop ? allFiles : [element.files[0]]);
}
-
- allFiles = allFiles.map(file => {
- if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
- return file;
- });
-
+ allFiles = await Promise.all(
+ allFiles.map(async (file) => {
+ let decryptedFile = file;
+ try {
+ const decryptFile = new DecryptFile();
+ const {isEncrypted, requiresPassword} = await decryptFile.checkFileEncrypted(file);
+ if (file.type === 'application/pdf' && isEncrypted) {
+ decryptedFile = await decryptFile.decryptFile(file, requiresPassword);
+ if (!decryptedFile) throw new Error('File decryption failed.');
+ }
+ decryptedFile.uniqueId = UUID.uuidv4();
+ return decryptedFile;
+ } catch (error) {
+ console.error(`Error decrypting file: ${file.name}`, error);
+ if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
+ return file;
+ }
+ })
+ );
if (!isDragAndDrop) {
- let dataTransfer = toDataTransfer(allFiles);
- element.files = dataTransfer.files;
+ let dataTransfer = toDataTransfer(allFiles);
+ element.files = dataTransfer.files;
}
handleFileInputChange(this);
- this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
-});
+ this.dispatchEvent(new CustomEvent('file-input-change', {bubbles: true, detail: {elementId, allFiles}}));
+ });
function toDataTransfer(files) {
let dataTransfer = new DataTransfer();
- files.forEach(file => dataTransfer.items.add(file));
+ files.forEach((file) => dataTransfer.items.add(file));
return dataTransfer;
}
@@ -136,7 +148,7 @@ function setupFileInput(chooser) {
const filesInfo = files.map((f) => ({name: f.name, size: f.size, uniqueId: f.uniqueId}));
- const selectedFilesContainer = $(inputContainer).siblings(".selected-files");
+ const selectedFilesContainer = $(inputContainer).siblings('.selected-files');
selectedFilesContainer.empty();
filesInfo.forEach((info) => {
let fileContainerClasses = 'small-file-container d-flex flex-column justify-content-center align-items-center';
@@ -167,28 +179,26 @@ function setupFileInput(chooser) {
}
function showOrHideSelectedFilesContainer(files) {
- if (files && files.length > 0)
- chooser.style.setProperty('--selected-files-display', 'flex');
- else
- chooser.style.setProperty('--selected-files-display', 'none');
+ if (files && files.length > 0) chooser.style.setProperty('--selected-files-display', 'flex');
+ else chooser.style.setProperty('--selected-files-display', 'none');
}
function removeFileListener(e) {
- const fileId = (e.target).getAttribute('data-file-id');
+ const fileId = e.target.getAttribute('data-file-id');
let inputElement = document.getElementById(elementId);
removeFileById(fileId, inputElement);
showOrHideSelectedFilesContainer(allFiles);
- inputElement.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
+ inputElement.dispatchEvent(new CustomEvent('file-input-change', {bubbles: true}));
}
function removeFileById(fileId, inputElement) {
let fileContainer = document.getElementById(fileId);
fileContainer.remove();
- allFiles = allFiles.filter(v => v.uniqueId != fileId);
+ allFiles = allFiles.filter((v) => v.uniqueId != fileId);
let dataTransfer = toDataTransfer(allFiles);
if (inputElement) inputElement.files = dataTransfer.files;
@@ -207,23 +217,19 @@ function setupFileInput(chooser) {
}
function createFileInfoContainer(info) {
- let fileInfoContainer = document.createElement("div");
+ let fileInfoContainer = document.createElement('div');
let fileInfoContainerClasses = 'file-info d-flex flex-column align-items-center justify-content-center';
$(fileInfoContainer).addClass(fileInfoContainerClasses);
- $(fileInfoContainer).append(
- `${info.name}
`
- );
+ $(fileInfoContainer).append(`${info.name}
`);
let fileSizeWithUnits = FileUtils.transformFileSize(info.size);
- $(fileInfoContainer).append(
- `${fileSizeWithUnits}
`
- );
+ $(fileInfoContainer).append(`${fileSizeWithUnits}
`);
return fileInfoContainer;
}
//Listen for event of file being removed and the filter it out of the allFiles array
- document.addEventListener("fileRemoved", function (e) {
+ document.addEventListener('fileRemoved', function (e) {
const fileId = e.detail;
let inputElement = document.getElementById(elementId);
removeFileById(fileId, inputElement);
diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js
index 4eaf43f1..c060203d 100644
--- a/src/main/resources/static/js/multitool/PdfContainer.js
+++ b/src/main/resources/static/js/multitool/PdfContainer.js
@@ -5,6 +5,7 @@ import {SplitAllCommand} from './commands/split.js';
import {UndoManager} from './UndoManager.js';
import {PageBreakCommand} from './commands/page-break.js';
import {AddFilesCommand} from './commands/add-page.js';
+import {DecryptFile} from '../DecryptFiles.js';
class PdfContainer {
fileName;
@@ -40,6 +41,8 @@ class PdfContainer {
this.removeAllElements = this.removeAllElements.bind(this);
this.resetPages = this.resetPages.bind(this);
+ this.decryptFile = new DecryptFile();
+
this.undoManager = undoManager || new UndoManager();
this.pdfAdapters = pdfAdapters;
@@ -165,7 +168,6 @@ class PdfContainer {
input.click();
});
}
-
async addFilesFromFiles(files, nextSiblingElement, pages) {
this.fileName = files[0].name;
for (var i = 0; i < files.length; i++) {
@@ -173,17 +175,37 @@ class PdfContainer {
let processingTime,
errorMessage = null,
pageCount = 0;
+
try {
- const file = files[i];
- if (file.type === 'application/pdf') {
- const {renderer, pdfDocument} = await this.loadFile(file);
+ let decryptedFile = files[i];
+ let isEncrypted = false;
+ let requiresPassword = false;
+ await this.decryptFile
+ .checkFileEncrypted(decryptedFile)
+ .then((result) => {
+ isEncrypted = result.isEncrypted;
+ requiresPassword = result.requiresPassword;
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ if (decryptedFile.type === 'application/pdf' && isEncrypted) {
+ decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
+ if (!decryptedFile) {
+ throw new Error('File decryption failed.');
+ }
+ }
+
+ if (decryptedFile.type === 'application/pdf') {
+ const {renderer, pdfDocument} = await this.loadFile(decryptedFile);
pageCount = renderer.pageCount || 0;
pages = await this.addPdfFile(renderer, pdfDocument, nextSiblingElement, pages);
- } else if (file.type.startsWith('image/')) {
- pages = await this.addImageFile(file, nextSiblingElement, pages);
+ } else if (decryptedFile.type.startsWith('image/')) {
+ pages = await this.addImageFile(decryptedFile, nextSiblingElement, pages);
}
+
processingTime = Date.now() - startTime;
- this.captureFileProcessingEvent(true, file, processingTime, null, pageCount);
+ this.captureFileProcessingEvent(true, decryptedFile, processingTime, null, pageCount);
} catch (error) {
processingTime = Date.now() - startTime;
errorMessage = error.message || 'Unknown error';
@@ -194,6 +216,7 @@ class PdfContainer {
document.querySelectorAll('.enable-on-file').forEach((element) => {
element.disabled = false;
});
+
return pages;
}
diff --git a/src/main/resources/static/js/pages/add-image.js b/src/main/resources/static/js/pages/add-image.js
new file mode 100644
index 00000000..5899b53f
--- /dev/null
+++ b/src/main/resources/static/js/pages/add-image.js
@@ -0,0 +1,47 @@
+document.getElementById('download-pdf').addEventListener('click', async () => {
+ const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
+ const modifiedPdfBytes = await modifiedPdf.save();
+ const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = originalFileName + '_addedImage.pdf';
+ link.click();
+});
+let originalFileName = '';
+document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
+ const fileInput = event.target;
+ fileInput.addEventListener('file-input-change', async (e) => {
+ const {allFiles} = e.detail;
+ if (allFiles && allFiles.length > 0) {
+ const file = allFiles[0];
+ originalFileName = file.name.replace(/\.[^/.]+$/, '');
+ const pdfData = await file.arrayBuffer();
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ 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';
+ });
+});
+
+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);
+ };
+ }
+});
diff --git a/src/main/resources/static/js/pages/adjust-contrast.js b/src/main/resources/static/js/pages/adjust-contrast.js
new file mode 100644
index 00000000..792c0666
--- /dev/null
+++ b/src/main/resources/static/js/pages/adjust-contrast.js
@@ -0,0 +1,253 @@
+var canvas = document.getElementById('contrast-pdf-canvas');
+var context = canvas.getContext('2d');
+var originalImageData = null;
+var allPages = [];
+var pdfDoc = null;
+var pdf = null; // This is the current PDF document
+
+async function renderPDFAndSaveOriginalImageData(file) {
+ var fileReader = new FileReader();
+ fileReader.onload = async function () {
+ var data = new Uint8Array(this.result);
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ pdf = await pdfjsLib.getDocument({data: data}).promise;
+
+ // Get the number of pages in the PDF
+ var numPages = pdf.numPages;
+ allPages = Array.from({length: numPages}, (_, i) => i + 1);
+
+ // Create a new PDF document
+ pdfDoc = await PDFLib.PDFDocument.create();
+ // Render the first page in the viewer
+ await renderPageAndAdjustImageProperties(1);
+ document.getElementById('sliders-container').style.display = 'block';
+ };
+ fileReader.readAsArrayBuffer(file);
+}
+
+// This function is now async and returns a promise
+function renderPageAndAdjustImageProperties(pageNum) {
+ return new Promise(async function (resolve, reject) {
+ var page = await pdf.getPage(pageNum);
+ var scale = 1.5;
+ var viewport = page.getViewport({scale: scale});
+
+ canvas.height = viewport.height;
+ canvas.width = viewport.width;
+
+ var renderContext = {
+ canvasContext: context,
+ viewport: viewport,
+ };
+
+ var renderTask = page.render(renderContext);
+ renderTask.promise.then(function () {
+ originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
+ adjustImageProperties();
+ resolve();
+ });
+ canvas.classList.add('fixed-shadow-canvas');
+ });
+}
+
+function adjustImageProperties() {
+ var contrast = parseFloat(document.getElementById('contrast-slider').value);
+ var brightness = parseFloat(document.getElementById('brightness-slider').value);
+ var saturation = parseFloat(document.getElementById('saturation-slider').value);
+
+ contrast /= 100; // normalize to range [0, 2]
+ brightness /= 100; // normalize to range [0, 2]
+ saturation /= 100; // normalize to range [0, 2]
+
+ if (originalImageData) {
+ var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
+ newImageData.data.set(originalImageData.data);
+
+ for (var i = 0; i < newImageData.data.length; i += 4) {
+ var r = newImageData.data[i];
+ var g = newImageData.data[i + 1];
+ var b = newImageData.data[i + 2];
+ // Adjust contrast
+ r = adjustContrastForPixel(r, contrast);
+ g = adjustContrastForPixel(g, contrast);
+ b = adjustContrastForPixel(b, contrast);
+ // Adjust brightness
+ r = adjustBrightnessForPixel(r, brightness);
+ g = adjustBrightnessForPixel(g, brightness);
+ b = adjustBrightnessForPixel(b, brightness);
+ // Adjust saturation
+ var rgb = adjustSaturationForPixel(r, g, b, saturation);
+ newImageData.data[i] = rgb[0];
+ newImageData.data[i + 1] = rgb[1];
+ newImageData.data[i + 2] = rgb[2];
+ }
+ context.putImageData(newImageData, 0, 0);
+ }
+}
+
+function rgbToHsl(r, g, b) {
+ (r /= 255), (g /= 255), (b /= 255);
+
+ var max = Math.max(r, g, b),
+ min = Math.min(r, g, b);
+ var h,
+ s,
+ l = (max + min) / 2;
+
+ if (max === min) {
+ h = s = 0; // achromatic
+ } else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+
+ h /= 6;
+ }
+
+ return [h, s, l];
+}
+
+function hslToRgb(h, s, l) {
+ var r, g, b;
+
+ if (s === 0) {
+ r = g = b = l; // achromatic
+ } else {
+ var hue2rgb = function hue2rgb(p, q, t) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+
+ return [r * 255, g * 255, b * 255];
+}
+
+function adjustContrastForPixel(pixel, contrast) {
+ // Normalize to range [-0.5, 0.5]
+ var normalized = pixel / 255 - 0.5;
+
+ // Apply contrast
+ normalized *= contrast;
+
+ // Denormalize back to [0, 255]
+ return (normalized + 0.5) * 255;
+}
+
+function clamp(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+}
+
+function adjustSaturationForPixel(r, g, b, saturation) {
+ var hsl = rgbToHsl(r, g, b);
+
+ // Adjust saturation
+ hsl[1] = clamp(hsl[1] * saturation, 0, 1);
+
+ // Convert back to RGB
+ var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
+
+ // Return adjusted RGB values
+ return rgb;
+}
+
+function adjustBrightnessForPixel(pixel, brightness) {
+ return Math.max(0, Math.min(255, pixel * brightness));
+}
+let inputFileName = '';
+async function downloadPDF() {
+ for (var i = 0; i < allPages.length; i++) {
+ await renderPageAndAdjustImageProperties(allPages[i]);
+ const pngImageBytes = canvas.toDataURL('image/png');
+ const pngImage = await pdfDoc.embedPng(pngImageBytes);
+ const pngDims = pngImage.scale(1);
+
+ // Create a blank page matching the dimensions of the image
+ const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
+
+ // Draw the PNG image
+ page.drawImage(pngImage, {
+ x: 0,
+ y: 0,
+ width: pngDims.width,
+ height: pngDims.height,
+ });
+ }
+
+ // Serialize the PDFDocument to bytes (a Uint8Array)
+ const pdfBytes = await pdfDoc.save();
+
+ // Create a Blob
+ const blob = new Blob([pdfBytes.buffer], {type: 'application/pdf'});
+
+ // Create download link
+ const downloadLink = document.createElement('a');
+ downloadLink.href = URL.createObjectURL(blob);
+ let newFileName = inputFileName ? inputFileName.replace('.pdf', '') : 'download';
+ newFileName += '_adjusted_color.pdf';
+
+ downloadLink.download = newFileName;
+ downloadLink.click();
+
+ // After download, reset the viewer and clear stored data
+ allPages = []; // Clear the pages
+ originalImageData = null; // Clear the image data
+
+ // Go back to page 1 and render it in the viewer
+ if (pdf !== null) {
+ renderPageAndAdjustImageProperties(1);
+ }
+}
+
+// Event listeners
+document.getElementById('fileInput-input').addEventListener('change', function (e) {
+ const fileInput = event.target;
+ fileInput.addEventListener('file-input-change', async (e) => {
+ const {allFiles} = e.detail;
+ if (allFiles && allFiles.length > 0) {
+ const file = allFiles[0];
+ inputFileName = file.name;
+ renderPDFAndSaveOriginalImageData(file);
+ }
+ });
+});
+
+document.getElementById('contrast-slider').addEventListener('input', function () {
+ document.getElementById('contrast-val').textContent = this.value;
+ adjustImageProperties();
+});
+
+document.getElementById('brightness-slider').addEventListener('input', function () {
+ document.getElementById('brightness-val').textContent = this.value;
+ adjustImageProperties();
+});
+
+document.getElementById('saturation-slider').addEventListener('input', function () {
+ document.getElementById('saturation-val').textContent = this.value;
+ adjustImageProperties();
+});
+
+document.getElementById('download-button').addEventListener('click', function () {
+ downloadPDF();
+});
diff --git a/src/main/resources/static/js/pages/change-metadata.js b/src/main/resources/static/js/pages/change-metadata.js
new file mode 100644
index 00000000..bdc5426b
--- /dev/null
+++ b/src/main/resources/static/js/pages/change-metadata.js
@@ -0,0 +1,150 @@
+const deleteAllCheckbox = document.querySelector('#deleteAll');
+let inputs = document.querySelectorAll('input');
+const customMetadataDiv = document.getElementById('customMetadata');
+const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
+
+deleteAllCheckbox.addEventListener('change', function (event) {
+ inputs.forEach((input) => {
+ // If it's the deleteAllCheckbox or any file input, skip
+ if (input === deleteAllCheckbox || input.type === 'file') {
+ return;
+ }
+ // Disable or enable based on the checkbox state
+ input.disabled = deleteAllCheckbox.checked;
+ });
+});
+
+const customModeCheckbox = document.getElementById('customModeCheckbox');
+const addMetadataBtn = document.getElementById('addMetadataBtn');
+const customMetadataFormContainer = document.getElementById('customMetadataEntries');
+var count = 1;
+const fileInput = document.querySelector('#fileInput-input');
+const authorInput = document.querySelector('#author');
+const creationDateInput = document.querySelector('#creationDate');
+const creatorInput = document.querySelector('#creator');
+const keywordsInput = document.querySelector('#keywords');
+const modificationDateInput = document.querySelector('#modificationDate');
+const producerInput = document.querySelector('#producer');
+const subjectInput = document.querySelector('#subject');
+const titleInput = document.querySelector('#title');
+const trappedInput = document.querySelector('#trapped');
+var lastPDFFileMeta = null;
+var lastPDFFile = null;
+
+fileInput.addEventListener('change', async function () {
+ fileInput.addEventListener('file-input-change', async (e) => {
+ const {allFiles} = e.detail;
+ if (allFiles && allFiles.length > 0) {
+ const file = allFiles[0];
+ while (otherMetadataEntriesDiv.firstChild) {
+ otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
+ }
+ while (customMetadataFormContainer.firstChild) {
+ customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
+ }
+ var url = URL.createObjectURL(file);
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ const pdf = await pdfjsLib.getDocument(url).promise;
+ const pdfMetadata = await pdf.getMetadata();
+ lastPDFFile = pdfMetadata?.info;
+ console.log(pdfMetadata);
+ if (!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
+ customModeCheckbox.disabled = true;
+ customModeCheckbox.checked = false;
+ } else {
+ customModeCheckbox.disabled = false;
+ }
+ authorInput.value = pdfMetadata?.info?.Author;
+ creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
+ creatorInput.value = pdfMetadata?.info?.Creator;
+ keywordsInput.value = pdfMetadata?.info?.Keywords;
+ modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
+ producerInput.value = pdfMetadata?.info?.Producer;
+ subjectInput.value = pdfMetadata?.info?.Subject;
+ titleInput.value = pdfMetadata?.info?.Title;
+ console.log(pdfMetadata?.info);
+ const trappedValue = pdfMetadata?.info?.Trapped;
+ // Get all options in the select element
+ const options = trappedInput.options;
+ // Loop through all options to find the one with a matching value
+ for (let i = 0; i < options.length; i++) {
+ if (options[i].value === trappedValue) {
+ options[i].selected = true;
+ break;
+ }
+ }
+ addExtra();
+ }
+ });
+});
+
+addMetadataBtn.addEventListener('click', () => {
+ const keyInput = document.createElement('input');
+ keyInput.type = 'text';
+ keyInput.placeholder = 'Key';
+ keyInput.className = 'form-control';
+ keyInput.name = `allRequestParams[customKey${count}]`;
+
+ const valueInput = document.createElement('input');
+ valueInput.type = 'text';
+ valueInput.placeholder = 'Value';
+ valueInput.className = 'form-control';
+ valueInput.name = `allRequestParams[customValue${count}]`;
+ count = count + 1;
+
+ const formGroup = document.createElement('div');
+ formGroup.className = 'mb-3';
+ formGroup.appendChild(keyInput);
+ formGroup.appendChild(valueInput);
+
+ customMetadataFormContainer.appendChild(formGroup);
+});
+function convertDateFormat(dateTimeString) {
+ if (!dateTimeString || dateTimeString.length < 17) {
+ return dateTimeString;
+ }
+
+ const year = dateTimeString.substring(2, 6);
+ const month = dateTimeString.substring(6, 8);
+ const day = dateTimeString.substring(8, 10);
+ const hour = dateTimeString.substring(10, 12);
+ const minute = dateTimeString.substring(12, 14);
+ const second = dateTimeString.substring(14, 16);
+
+ return year + '/' + month + '/' + day + ' ' + hour + ':' + minute + ':' + second;
+}
+
+function addExtra() {
+ const event = document.getElementById('customModeCheckbox');
+ if (event.checked && lastPDFFile.Custom != null) {
+ customMetadataDiv.style.display = 'block';
+ for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
+ if (
+ key === 'Author' ||
+ key === 'CreationDate' ||
+ key === 'Creator' ||
+ key === 'Keywords' ||
+ key === 'ModDate' ||
+ key === 'Producer' ||
+ key === 'Subject' ||
+ key === 'Title' ||
+ key === 'Trapped'
+ ) {
+ continue;
+ }
+ const entryDiv = document.createElement('div');
+ entryDiv.className = 'mb-3';
+ entryDiv.innerHTML = ``;
+ otherMetadataEntriesDiv.appendChild(entryDiv);
+ }
+ } else {
+ customMetadataDiv.style.display = 'none';
+ while (otherMetadataEntriesDiv.firstChild) {
+ otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
+ }
+ }
+}
+
+customModeCheckbox.addEventListener('change', (event) => {
+ addExtra();
+});
diff --git a/src/main/resources/static/js/pages/crop.js b/src/main/resources/static/js/pages/crop.js
new file mode 100644
index 00000000..1854023a
--- /dev/null
+++ b/src/main/resources/static/js/pages/crop.js
@@ -0,0 +1,159 @@
+let pdfCanvas = document.getElementById('cropPdfCanvas');
+let overlayCanvas = document.getElementById('overlayCanvas');
+let canvasesContainer = document.getElementById('canvasesContainer');
+canvasesContainer.style.display = 'none';
+let containerRect = canvasesContainer.getBoundingClientRect();
+
+let context = pdfCanvas.getContext('2d');
+let overlayContext = overlayCanvas.getContext('2d');
+
+overlayCanvas.width = pdfCanvas.width;
+overlayCanvas.height = pdfCanvas.height;
+
+let isDrawing = false; // New flag to check if drawing is ongoing
+
+let cropForm = document.getElementById('cropForm');
+let fileInput = document.getElementById('fileInput-input');
+let xInput = document.getElementById('x');
+let yInput = document.getElementById('y');
+let widthInput = document.getElementById('width');
+let heightInput = document.getElementById('height');
+
+let pdfDoc = null;
+let currentPage = 1;
+let totalPages = 0;
+
+let startX = 0;
+let startY = 0;
+let rectWidth = 0;
+let rectHeight = 0;
+
+let pageScale = 1; // The scale which the pdf page renders
+let timeId = null; // timeout id for resizing canvases event
+
+function renderPageFromFile(file) {
+ if (file.type === 'application/pdf') {
+ let reader = new FileReader();
+ reader.onload = function (ev) {
+ let typedArray = new Uint8Array(reader.result);
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
+ pdfDoc = pdf;
+ totalPages = pdf.numPages;
+ renderPage(currentPage);
+ });
+ };
+ reader.readAsArrayBuffer(file);
+ }
+}
+
+window.addEventListener('resize', function () {
+ clearTimeout(timeId);
+
+ timeId = setTimeout(function () {
+ if (fileInput.files.length == 0) return;
+ let canvasesContainer = document.getElementById('canvasesContainer');
+ let containerRect = canvasesContainer.getBoundingClientRect();
+
+ context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
+
+ overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
+
+ pdfCanvas.width = containerRect.width;
+ pdfCanvas.height = containerRect.height;
+
+ overlayCanvas.width = containerRect.width;
+ overlayCanvas.height = containerRect.height;
+
+ let file = fileInput.files[0];
+ renderPageFromFile(file);
+ }, 1000);
+});
+
+fileInput.addEventListener('change', function (e) {
+ fileInput.addEventListener('file-input-change', async (e) => {
+ const {allFiles} = e.detail;
+ if (allFiles && allFiles.length > 0) {
+ canvasesContainer.style.display = 'block'; // set for visual purposes
+ let file = allFiles[0];
+ renderPageFromFile(file);
+ }
+ });
+});
+
+cropForm.addEventListener('submit', function (e) {
+ if (xInput.value == '' && yInput.value == '' && widthInput.value == '' && heightInput.value == '') {
+ // Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
+ xInput.value = 0;
+ yInput.value = 0;
+ widthInput.value = containerRect.width;
+ heightInput.value = containerRect.height;
+ }
+});
+
+overlayCanvas.addEventListener('mousedown', function (e) {
+ // Clear previously drawn rectangle on the main canvas
+ context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
+ renderPage(currentPage); // Re-render the PDF
+
+ // Clear the overlay canvas to ensure old drawings are removed
+ overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
+
+ startX = e.offsetX;
+ startY = e.offsetY;
+ isDrawing = true;
+});
+
+overlayCanvas.addEventListener('mousemove', function (e) {
+ if (!isDrawing) return;
+ overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
+
+ rectWidth = e.offsetX - startX;
+ rectHeight = e.offsetY - startY;
+ overlayContext.strokeStyle = 'red';
+ overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
+});
+
+overlayCanvas.addEventListener('mouseup', function (e) {
+ isDrawing = false;
+
+ rectWidth = e.offsetX - startX;
+ rectHeight = e.offsetY - startY;
+
+ let flippedY = pdfCanvas.height - e.offsetY;
+
+ xInput.value = startX / pageScale;
+ yInput.value = flippedY / pageScale;
+ widthInput.value = rectWidth / pageScale;
+ heightInput.value = rectHeight / pageScale;
+
+ // Draw the final rectangle on the main canvas
+ context.strokeStyle = 'red';
+ context.strokeRect(startX, startY, rectWidth, rectHeight);
+
+ overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
+});
+
+function renderPage(pageNumber) {
+ pdfDoc.getPage(pageNumber).then(function (page) {
+ let canvasesContainer = document.getElementById('canvasesContainer');
+ let containerRect = canvasesContainer.getBoundingClientRect();
+
+ pageScale = containerRect.width / page.getViewport({scale: 1}).width; // The new scale
+
+ let viewport = page.getViewport({scale: containerRect.width / page.getViewport({scale: 1}).width});
+
+ canvasesContainer.width = viewport.width;
+ canvasesContainer.height = viewport.height;
+
+ pdfCanvas.width = viewport.width;
+ pdfCanvas.height = viewport.height;
+
+ overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
+ overlayCanvas.height = viewport.height;
+
+ let renderContext = {canvasContext: context, viewport: viewport};
+ page.render(renderContext);
+ pdfCanvas.classList.add('shadow-canvas');
+ });
+}
diff --git a/src/main/resources/static/js/pages/pdf-to-csv.js b/src/main/resources/static/js/pages/pdf-to-csv.js
new file mode 100644
index 00000000..6be3c2ed
--- /dev/null
+++ b/src/main/resources/static/js/pages/pdf-to-csv.js
@@ -0,0 +1,138 @@
+let pdfCanvas = document.getElementById('cropPdfCanvas');
+let overlayCanvas = document.getElementById('overlayCanvas');
+let canvasesContainer = document.getElementById('canvasesContainer');
+canvasesContainer.style.display = 'none';
+// let paginationBtnContainer = ;
+
+let context = pdfCanvas.getContext('2d');
+let overlayContext = overlayCanvas.getContext('2d');
+
+let btn1Object = document.getElementById('previous-page-btn');
+let btn2Object = document.getElementById('next-page-btn');
+overlayCanvas.width = pdfCanvas.width;
+overlayCanvas.height = pdfCanvas.height;
+
+let fileInput = document.getElementById('fileInput-input');
+
+let file;
+
+let pdfDoc = null;
+let pageId = document.getElementById('pageId');
+let currentPage = 1;
+let totalPages = 0;
+
+let startX = 0;
+let startY = 0;
+let rectWidth = 0;
+let rectHeight = 0;
+
+let timeId = null; // timeout id for resizing canvases event
+
+btn1Object.addEventListener('click', function (e) {
+ if (currentPage !== 1) {
+ currentPage = currentPage - 1;
+ pageId.value = currentPage;
+
+ if (file.type === 'application/pdf') {
+ let reader = new FileReader();
+ reader.onload = function (ev) {
+ let typedArray = new Uint8Array(reader.result);
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
+ pdfDoc = pdf;
+ totalPages = pdf.numPages;
+ renderPage(currentPage);
+ });
+ };
+ reader.readAsArrayBuffer(file);
+ }
+ }
+});
+
+btn2Object.addEventListener('click', function (e) {
+ if (currentPage !== totalPages) {
+ currentPage = currentPage + 1;
+ pageId.value = currentPage;
+
+ if (file.type === 'application/pdf') {
+ let reader = new FileReader();
+ reader.onload = function (ev) {
+ let typedArray = new Uint8Array(reader.result);
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
+ pdfDoc = pdf;
+ totalPages = pdf.numPages;
+ renderPage(currentPage);
+ });
+ };
+ reader.readAsArrayBuffer(file);
+ }
+ }
+});
+
+function renderPageFromFile(file) {
+ if (file.type === 'application/pdf') {
+ let reader = new FileReader();
+ reader.onload = function (ev) {
+ let typedArray = new Uint8Array(reader.result);
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
+ pdfDoc = pdf;
+ totalPages = pdf.numPages;
+ renderPage(currentPage);
+ });
+ pageId.value = currentPage;
+ };
+ reader.readAsArrayBuffer(file);
+ document.getElementById('pagination-button-container').style.display = 'flex';
+ document.getElementById('instruction-text').style.display = 'block';
+ }
+}
+
+window.addEventListener('resize', function () {
+ clearTimeout(timeId);
+ timeId = setTimeout(function () {
+ if (fileInput.files.length == 0) return;
+ let canvasesContainer = document.getElementById('canvasesContainer');
+ let containerRect = canvasesContainer.getBoundingClientRect();
+
+ context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
+
+ overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
+
+ pdfCanvas.width = containerRect.width;
+ pdfCanvas.height = containerRect.height;
+
+ overlayCanvas.width = containerRect.width;
+ overlayCanvas.height = containerRect.height;
+
+ let file = fileInput.files[0];
+ renderPageFromFile(file);
+ }, 1000);
+});
+
+fileInput.addEventListener('change', function (e) {
+ fileInput.addEventListener('file-input-change', async (e) => {
+ const {allFiles} = e.detail;
+ if (allFiles && allFiles.length > 0) {
+ canvasesContainer.style.display = 'block'; // set for visual purposes
+ file = e.target.files[0];
+ renderPageFromFile(file);
+ }
+ });
+});
+
+function renderPage(pageNumber) {
+ pdfDoc.getPage(pageNumber).then(function (page) {
+ let viewport = page.getViewport({scale: 1.0});
+ pdfCanvas.width = viewport.width;
+ pdfCanvas.height = viewport.height;
+
+ overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
+ overlayCanvas.height = viewport.height;
+
+ let renderContext = {canvasContext: context, viewport: viewport};
+ page.render(renderContext);
+ pdfCanvas.classList.add('shadow-canvas');
+ });
+}
diff --git a/src/main/resources/static/js/pages/sign.js b/src/main/resources/static/js/pages/sign.js
new file mode 100644
index 00000000..8d45c969
--- /dev/null
+++ b/src/main/resources/static/js/pages/sign.js
@@ -0,0 +1,213 @@
+window.toggleSignatureView = toggleSignatureView;
+window.previewSignature = previewSignature;
+window.addSignatureFromPreview = addSignatureFromPreview;
+window.addDraggableFromPad = addDraggableFromPad;
+window.addDraggableFromText = addDraggableFromText;
+window.goToFirstOrLastPage = goToFirstOrLastPage;
+
+let currentPreviewSrc = null;
+
+function toggleSignatureView() {
+ const gridView = document.getElementById('gridView');
+ const listView = document.getElementById('listView');
+ const gridText = document.querySelector('.grid-view-text');
+ const listText = document.querySelector('.list-view-text');
+
+ if (gridView.style.display !== 'none') {
+ gridView.style.display = 'none';
+ listView.style.display = 'block';
+ gridText.style.display = 'none';
+ listText.style.display = 'inline';
+ } else {
+ gridView.style.display = 'block';
+ listView.style.display = 'none';
+ gridText.style.display = 'inline';
+ listText.style.display = 'none';
+ }
+}
+
+function previewSignature(element) {
+ const src = element.dataset.src;
+ currentPreviewSrc = src;
+
+ const filename = element.querySelector('.signature-list-name').textContent;
+
+ const previewImage = document.getElementById('previewImage');
+ const previewFileName = document.getElementById('previewFileName');
+
+ previewImage.src = src;
+ previewFileName.textContent = filename;
+
+ const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
+ modal.show();
+}
+
+function addSignatureFromPreview() {
+ if (currentPreviewSrc) {
+ DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
+ bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
+ }
+}
+
+let originalFileName = '';
+document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
+ const fileInput = event.target;
+ fileInput.addEventListener('file-input-change', async (e) => {
+ const {allFiles} = e.detail;
+ if (allFiles && allFiles.length > 0) {
+ const file = allFiles[0];
+ originalFileName = file.name.replace(/\.[^/.]+$/, '');
+ const pdfData = await file.arrayBuffer();
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ 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';
+ });
+});
+
+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);
+ };
+ }
+});
+
+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) {
+ 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() {
+ 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);
+
+ signaturePad.clear();
+}
+
+new IntersectionObserver((entries, observer) => {
+ if (entries.some((entry) => entry.intersectionRatio > 0)) {
+ resizeCanvas();
+ }
+}).observe(signaturePadCanvas);
+
+new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
+
+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;
+
+ let paragraphs = sigText.split(/\r?\n/);
+
+ canvas.width = textWidth;
+ canvas.height = paragraphs.length * textHeight * 1.35; // for tails
+ ctx.font = `${fontSize}px ${font}`;
+
+ ctx.textBaseline = 'top';
+
+ let y = 0;
+
+ paragraphs.forEach((paragraph) => {
+ ctx.fillText(paragraph, 0, y);
+ y += fontSize;
+ });
+
+ const dataURL = canvas.toDataURL();
+ DraggableUtils.createDraggableCanvasFromUrl(dataURL);
+}
+
+async function goToFirstOrLastPage(page) {
+ if (page) {
+ const lastPage = DraggableUtils.pdfDoc.numPages;
+ await DraggableUtils.goToPage(lastPage - 1);
+ } else {
+ await DraggableUtils.goToPage(0);
+ }
+}
+
+document.getElementById('download-pdf').addEventListener('click', async () => {
+ const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
+ const modifiedPdfBytes = await modifiedPdf.save();
+ const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = originalFileName + '_signed.pdf';
+ link.click();
+});
diff --git a/src/main/resources/templates/convert/pdf-to-csv.html b/src/main/resources/templates/convert/pdf-to-csv.html
index b3dfc09c..97683734 100644
--- a/src/main/resources/templates/convert/pdf-to-csv.html
+++ b/src/main/resources/templates/convert/pdf-to-csv.html
@@ -34,140 +34,8 @@
-
+
diff --git a/src/main/resources/templates/crop.html b/src/main/resources/templates/crop.html
index e606fded..5262f0f5 100644
--- a/src/main/resources/templates/crop.html
+++ b/src/main/resources/templates/crop.html
@@ -32,163 +32,7 @@
-
+
diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html
index 314d28b3..00f22c27 100644
--- a/src/main/resources/templates/error.html
+++ b/src/main/resources/templates/error.html
@@ -19,7 +19,7 @@
diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html
index f372d1dd..37633704 100644
--- a/src/main/resources/templates/fragments/common.html
+++ b/src/main/resources/templates/fragments/common.html
@@ -203,7 +203,17 @@
-
+
-
+
diff --git a/src/main/resources/templates/misc/add-image.html b/src/main/resources/templates/misc/add-image.html
index 947185a7..6bca8ee7 100644
--- a/src/main/resources/templates/misc/add-image.html
+++ b/src/main/resources/templates/misc/add-image.html
@@ -22,46 +22,9 @@
-
-
+
@@ -93,17 +56,6 @@
-
diff --git a/src/main/resources/templates/misc/adjust-contrast.html b/src/main/resources/templates/misc/adjust-contrast.html
index 22ef855d..4114955e 100644
--- a/src/main/resources/templates/misc/adjust-contrast.html
+++ b/src/main/resources/templates/misc/adjust-contrast.html
@@ -55,247 +55,7 @@
-
+
diff --git a/src/main/resources/templates/misc/change-metadata.html b/src/main/resources/templates/misc/change-metadata.html
index 50ba318b..2aaa47e9 100644
--- a/src/main/resources/templates/misc/change-metadata.html
+++ b/src/main/resources/templates/misc/change-metadata.html
@@ -85,140 +85,7 @@
-
diff --git a/src/main/resources/templates/misc/extract-images.html b/src/main/resources/templates/misc/extract-images.html
index d47a72d7..1e6268fe 100644
--- a/src/main/resources/templates/misc/extract-images.html
+++ b/src/main/resources/templates/misc/extract-images.html
@@ -2,6 +2,7 @@
+
diff --git a/src/main/resources/templates/misc/replace-color.html b/src/main/resources/templates/misc/replace-color.html
index 4defcff7..4ab5460a 100644
--- a/src/main/resources/templates/misc/replace-color.html
+++ b/src/main/resources/templates/misc/replace-color.html
@@ -4,6 +4,7 @@
+
diff --git a/src/main/resources/templates/multi-tool.html b/src/main/resources/templates/multi-tool.html
index 14e5a85e..17eafedf 100644
--- a/src/main/resources/templates/multi-tool.html
+++ b/src/main/resources/templates/multi-tool.html
@@ -162,10 +162,19 @@
insertPageBreak:'[[#{multiTool.insertPageBreak}]]',
dragDropMessage:'[[#{multiTool.dragDropMessage}]]',
undo: '[[#{multiTool.undo}]]',
- redo: '[[#{multiTool.redo}]]'
+ redo: '[[#{multiTool.redo}]]',
};
-
+ window.decrypt = {
+ passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
+ cancelled: '[[#{decrypt.cancelled}]]',
+ noPassword: '[[#{decrypt.noPassword}]]',
+ invalidPassword: '[[#{decrypt.invalidPassword}]]',
+ invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
+ unexpectedError: '[[#{decrypt.unexpectedError}]]',
+ serverError: '[[#{decrypt.serverError}]]',
+ success: '[[#{decrypt.success}]]',
+ }
const csvInput = document.getElementById("csv-input");
csvInput.addEventListener("keydown", function (event) {
diff --git a/src/main/resources/templates/overlay-pdf.html b/src/main/resources/templates/overlay-pdf.html
index e92e4cb0..616e3343 100644
--- a/src/main/resources/templates/overlay-pdf.html
+++ b/src/main/resources/templates/overlay-pdf.html
@@ -2,6 +2,7 @@
+
diff --git a/src/main/resources/templates/pipeline.html b/src/main/resources/templates/pipeline.html
index eaa87463..dae05df2 100644
--- a/src/main/resources/templates/pipeline.html
+++ b/src/main/resources/templates/pipeline.html
@@ -14,6 +14,7 @@
th:href="@{'/css/pipeline.css'}"
th:if="${currentPage == 'pipeline'}"
/>
+
+
+
@@ -42,75 +43,6 @@
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
-
-
-
-
-
-
@@ -410,35 +225,11 @@
-
-
-
-
-
@@ -447,6 +238,7 @@
+
\ No newline at end of file