From ef8231de3a329812e6c8e1c6a1fff9841643e727 Mon Sep 17 00:00:00 2001 From: Reece Browne Date: Tue, 10 Dec 2024 16:39:06 +0000 Subject: [PATCH] Add Decrypt to all relevant pages --- .../static/js/{multitool => }/DecryptFiles.js | 20 +- src/main/resources/static/js/download.js | 27 ++ .../resources/static/js/draggable-utils.js | 123 ++++----- src/main/resources/static/js/fileInput.js | 98 +++---- .../static/js/multitool/PdfContainer.js | 2 +- .../static/js/pages/adjust-contrast.js | 253 ++++++++++++++++++ src/main/resources/static/js/pages/sign.js | 215 +++++++++++++++ .../resources/templates/fragments/common.html | 4 +- .../templates/misc/adjust-contrast.html | 242 +---------------- src/main/resources/templates/multi-tool.html | 19 +- src/main/resources/templates/sign.html | 214 +-------------- 11 files changed, 633 insertions(+), 584 deletions(-) rename src/main/resources/static/js/{multitool => }/DecryptFiles.js (85%) create mode 100644 src/main/resources/static/js/download.js create mode 100644 src/main/resources/static/js/pages/adjust-contrast.js create mode 100644 src/main/resources/static/js/pages/sign.js 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( - `
${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 bbc664f7b..c060203d1 100644 --- a/src/main/resources/static/js/multitool/PdfContainer.js +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -5,7 +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'; +import {DecryptFile} from '../DecryptFiles.js'; class PdfContainer { fileName; 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 000000000..792c06669 --- /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/sign.js b/src/main/resources/static/js/pages/sign.js new file mode 100644 index 000000000..411fabf49 --- /dev/null +++ b/src/main/resources/static/js/pages/sign.js @@ -0,0 +1,215 @@ +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; + + // Wait for the second function to complete + 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/fragments/common.html b/src/main/resources/templates/fragments/common.html index 04a91a8dd..37633704d 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -204,8 +204,7 @@
diff --git a/src/main/resources/templates/misc/adjust-contrast.html b/src/main/resources/templates/misc/adjust-contrast.html index 22ef855d7..4114955ee 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/multi-tool.html b/src/main/resources/templates/multi-tool.html index 260ad74cd..17eafedf3 100644 --- a/src/main/resources/templates/multi-tool.html +++ b/src/main/resources/templates/multi-tool.html @@ -163,17 +163,18 @@ dragDropMessage:'[[#{multiTool.dragDropMessage}]]', undo: '[[#{multiTool.undo}]]', redo: '[[#{multiTool.redo}]]', - passwordPrompt: '[[#{decrypt.passwordPrompt}]]', - cancelled: '[[#{decrypt.cancelled}]]', - noPassword: '[[#{decrypt.noPassword}]]', - invalidPassword: '[[#{decrypt.invalidPassword}]]', - invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]', - unexpectedError: '[[#{decrypt.unexpectedError}]]', - serverError: '[[#{decrypt.serverError}]]', - success: '[[#{decrypt.success}]]', }; - + 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/sign.html b/src/main/resources/templates/sign.html index 31c855dac..a0fe2f107 100644 --- a/src/main/resources/templates/sign.html +++ b/src/main/resources/templates/sign.html @@ -19,9 +19,10 @@ } - + + @@ -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