diff --git a/src/main/resources/static/js/multitool/ImageHighlighter.js b/src/main/resources/static/js/multitool/ImageHighlighter.js index b72df3bb..7fc53209 100644 --- a/src/main/resources/static/js/multitool/ImageHighlighter.js +++ b/src/main/resources/static/js/multitool/ImageHighlighter.js @@ -1,4 +1,4 @@ -class ImageHiglighter { +class ImageHighlighter { imageHighlighter; constructor(id) { this.imageHighlighter = document.getElementById(id); @@ -41,6 +41,25 @@ class ImageHiglighter { img.addEventListener("click", this.imageHighlightCallback); return div; } + + async addImageFile(file, nextSiblingElement) { + const div = document.createElement("div"); + div.classList.add("page-container"); + + var img = document.createElement("img"); + img.classList.add("page-image"); + img.src = URL.createObjectURL(file); + div.appendChild(img); + + this.pdfAdapters.forEach((adapter) => { + adapter.adapt?.(div); + }); + if (nextSiblingElement) { + this.pagesContainer.insertBefore(div, nextSiblingElement); + } else { + this.pagesContainer.appendChild(div); + } + } } -export default ImageHiglighter; +export default ImageHighlighter; diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js index 0b11658d..7f2ceebf 100644 --- a/src/main/resources/static/js/multitool/PdfContainer.js +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -10,27 +10,28 @@ class PdfContainer { this.pagesContainerWrapper = document.getElementById(wrapperId); this.downloadLink = null; this.movePageTo = this.movePageTo.bind(this); - this.addPdfs = this.addPdfs.bind(this); - this.addPdfsFromFiles = this.addPdfsFromFiles.bind(this); + this.addFiles = this.addFiles.bind(this); + this.addFilesFromFiles = this.addFilesFromFiles.bind(this); this.rotateElement = this.rotateElement.bind(this); this.rotateAll = this.rotateAll.bind(this); this.exportPdf = this.exportPdf.bind(this); this.updateFilename = this.updateFilename.bind(this); this.setDownloadAttribute = this.setDownloadAttribute.bind(this); this.preventIllegalChars = this.preventIllegalChars.bind(this); + this.addImageFile = this.addImageFile.bind(this); this.pdfAdapters = pdfAdapters; this.pdfAdapters.forEach((adapter) => { adapter.setActions({ movePageTo: this.movePageTo, - addPdfs: this.addPdfs, + addFiles: this.addFiles, rotateElement: this.rotateElement, updateFilename: this.updateFilename, }); }); - window.addPdfs = this.addPdfs; + window.addFiles = this.addFiles; window.exportPdf = this.exportPdf; window.rotateAll = this.rotateAll; @@ -65,25 +66,30 @@ class PdfContainer { } } - addPdfs(nextSiblingElement) { + addFiles(nextSiblingElement) { var input = document.createElement("input"); input.type = "file"; input.multiple = true; - input.setAttribute("accept", "application/pdf"); + input.setAttribute("accept", "application/pdf,image/*"); input.onchange = async (e) => { const files = e.target.files; - this.addPdfsFromFiles(files, nextSiblingElement); + this.addFilesFromFiles(files, nextSiblingElement); this.updateFilename(files ? files[0].name : ""); }; input.click(); } - async addPdfsFromFiles(files, nextSiblingElement) { + async addFilesFromFiles(files, nextSiblingElement) { this.fileName = files[0].name; for (var i = 0; i < files.length; i++) { - await this.addPdfFile(files[i], nextSiblingElement); + const file = files[i]; + if (file.type === "application/pdf") { + await this.addPdfFile(file, nextSiblingElement); + } else if (file.type.startsWith("image/")) { + await this.addImageFile(file, nextSiblingElement); + } } document.querySelectorAll(".enable-on-file").forEach((element) => { @@ -130,6 +136,25 @@ class PdfContainer { } } + async addImageFile(file, nextSiblingElement) { + const div = document.createElement("div"); + div.classList.add("page-container"); + + var img = document.createElement("img"); + img.classList.add("page-image"); + img.src = URL.createObjectURL(file); + div.appendChild(img); + + this.pdfAdapters.forEach((adapter) => { + adapter.adapt?.(div); + }); + if (nextSiblingElement) { + this.pagesContainer.insertBefore(div, nextSiblingElement); + } else { + this.pagesContainer.appendChild(div); + } + } + async loadFile(file) { var objectUrl = URL.createObjectURL(file); var pdfDocument = await this.toPdfLib(objectUrl); @@ -193,16 +218,47 @@ class PdfContainer { for (var i = 0; i < pageContainers.length; i++) { const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container if (!img) continue; - const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]); - const page = pages[0]; + let page; + if (img.doc) { + const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]); + page = pages[0]; + pdfDoc.addPage(page); + } else { + page = pdfDoc.addPage([img.naturalWidth, img.naturalHeight]); + const imageBytes = await fetch(img.src).then((res) => res.arrayBuffer()); + const uint8Array = new Uint8Array(imageBytes); + const imageType = detectImageType(uint8Array); + let image; + switch (imageType) { + case 'PNG': + image = await pdfDoc.embedPng(imageBytes); + break; + case 'JPEG': + image = await pdfDoc.embedJpg(imageBytes); + break; + case 'TIFF': + image = await pdfDoc.embedTiff(imageBytes); + break; + case 'GIF': + console.warn(`Unsupported image type: ${imageType}`); + continue; // Skip this image + default: + console.warn(`Unsupported image type: ${imageType}`); + continue; // Skip this image + } + page.drawImage(image, { + x: 0, + y: 0, + width: img.naturalWidth, + height: img.naturalHeight, + }); + } const rotation = img.style.rotate; if (rotation) { const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, "")); page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle)); } - - pdfDoc.addPage(page); } const pdfBytes = await pdfDoc.save(); const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" }); @@ -249,6 +305,7 @@ class PdfContainer { } } + setDownloadAttribute() { this.downloadLink.setAttribute("download", this.fileName ? this.fileName : "managed.pdf"); } @@ -280,5 +337,28 @@ class PdfContainer { // } } } +function detectImageType(uint8Array) { + // Check for PNG signature + if (uint8Array[0] === 137 && uint8Array[1] === 80 && uint8Array[2] === 78 && uint8Array[3] === 71) { + return 'PNG'; + } + // Check for JPEG signature + if (uint8Array[0] === 255 && uint8Array[1] === 216 && uint8Array[2] === 255) { + return 'JPEG'; + } + + // Check for TIFF signature (little-endian and big-endian) + if ((uint8Array[0] === 73 && uint8Array[1] === 73 && uint8Array[2] === 42 && uint8Array[3] === 0) || + (uint8Array[0] === 77 && uint8Array[1] === 77 && uint8Array[2] === 0 && uint8Array[3] === 42)) { + return 'TIFF'; + } + + // Check for GIF signature + if (uint8Array[0] === 71 && uint8Array[1] === 73 && uint8Array[2] === 70) { + return 'GIF'; + } + + return 'UNKNOWN'; +} export default PdfContainer; diff --git a/src/main/resources/static/js/multitool/fileInput.js b/src/main/resources/static/js/multitool/fileInput.js index 77455c06..9efa8eb2 100644 --- a/src/main/resources/static/js/multitool/fileInput.js +++ b/src/main/resources/static/js/multitool/fileInput.js @@ -90,6 +90,25 @@ class FileDragManager { this.updateFilename(files ? files[0].name : ""); }); } + + async addImageFile(file, nextSiblingElement) { + const div = document.createElement("div"); + div.classList.add("page-container"); + + var img = document.createElement("img"); + img.classList.add("page-image"); + img.src = URL.createObjectURL(file); + div.appendChild(img); + + this.pdfAdapters.forEach((adapter) => { + adapter.adapt?.(div); + }); + if (nextSiblingElement) { + this.pagesContainer.insertBefore(div, nextSiblingElement); + } else { + this.pagesContainer.appendChild(div); + } + } } export default FileDragManager; diff --git a/src/main/resources/templates/multi-tool.html b/src/main/resources/templates/multi-tool.html index b578d594..2bbbf086 100644 --- a/src/main/resources/templates/multi-tool.html +++ b/src/main/resources/templates/multi-tool.html @@ -27,7 +27,7 @@ th:placeholder="#{multiTool.uploadPrompts}">