diff --git a/src/main/resources/static/js/multitool/DecryptFiles.js b/src/main/resources/static/js/DecryptFiles.js similarity index 85% rename from src/main/resources/static/js/multitool/DecryptFiles.js rename to src/main/resources/static/js/DecryptFiles.js index 509c1acd8..b2dbcac49 100644 --- a/src/main/resources/static/js/multitool/DecryptFiles.js +++ b/src/main/resources/static/js/DecryptFiles.js @@ -4,7 +4,7 @@ export class DecryptFile { const formData = new FormData(); formData.append('fileInput', file); if (requiresPassword) { - const password = prompt(`${window.translations.passwordPrompt}`); + const password = prompt(`${window.decrypt.passwordPrompt}`); if (password === null) { // User cancelled @@ -16,9 +16,9 @@ export class DecryptFile { // No password provided console.error(`No password provided for encrypted PDF: ${file.name}`); this.showErrorBanner( - `${window.translations.noPassword.replace('{0}', file.name)}`, + `${window.decrypt.noPassword.replace('{0}', file.name)}`, '', - `${window.translations.unexpectedError}` + `${window.decrypt.unexpectedError}` ); return null; // No file to return } @@ -37,11 +37,11 @@ export class DecryptFile { return new File([decryptedBlob], file.name, {type: 'application/pdf'}); } else { const errorText = await response.text(); - console.error(`${window.translations.invalidPassword} ${errorText}`); + console.error(`${window.decrypt.invalidPassword} ${errorText}`); this.showErrorBanner( - `${window.translations.invalidPassword}`, + `${window.decrypt.invalidPassword}`, errorText, - `${window.translations.invalidPasswordHeader.replace('{0}', file.name)}` + `${window.decrypt.invalidPasswordHeader.replace('{0}', file.name)}` ); return null; // No file to return } @@ -49,8 +49,8 @@ export class DecryptFile { // Handle network or unexpected errors console.error(`Failed to decrypt PDF: ${file.name}`, error); this.showErrorBanner( - `${window.translations.unexpectedError.replace('{0}', file.name)}`, - `${error.message || window.translations.unexpectedError}`, + `${window.decrypt.unexpectedError.replace('{0}', file.name)}`, + `${error.message || window.decrypt.unexpectedError}`, error ); return null; // No file to return @@ -59,6 +59,10 @@ export class DecryptFile { 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); diff --git a/src/main/resources/static/js/download.js b/src/main/resources/static/js/download.js new file mode 100644 index 000000000..8eed99f9a --- /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/draggable-utils.js b/src/main/resources/static/js/draggable-utils.js index bdd75a44a..aa0c15a03 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 e288f5b84..57010b6c6 100644 --- a/src/main/resources/static/js/fileInput.js +++ b/src/main/resources/static/js/fileInput.js @@ -1,20 +1,20 @@ -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'; +const decryptFile = new DecryptFile(); 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 +26,7 @@ function setupFileInput(chooser) { inputContainer.addEventListener('click', (e) => { let inputBtn = document.getElementById(elementId); inputBtn.click(); - }) + }); const dragenterListener = function () { dragCounter++; @@ -63,7 +63,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 +78,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 +87,7 @@ function setupFileInput(chooser) { } } - ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => { document.body.addEventListener(eventName, preventDefaults, false); }); @@ -96,37 +96,49 @@ 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); + 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( - `