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

[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) -[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) +[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](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