Add image support to multi-tool page (#1769)

* Add image support to multi-tool page

Related to #278

* changes to support image types

* final touches

---------

Co-authored-by: a <a>
This commit is contained in:
Anthony Stirling 2024-08-29 11:07:31 +02:00 committed by GitHub
parent 316021453f
commit 68bc4d82de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 140 additions and 22 deletions

View file

@ -1,4 +1,4 @@
class ImageHiglighter { class ImageHighlighter {
imageHighlighter; imageHighlighter;
constructor(id) { constructor(id) {
this.imageHighlighter = document.getElementById(id); this.imageHighlighter = document.getElementById(id);
@ -41,6 +41,25 @@ class ImageHiglighter {
img.addEventListener("click", this.imageHighlightCallback); img.addEventListener("click", this.imageHighlightCallback);
return div; 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;

View file

@ -10,27 +10,28 @@ class PdfContainer {
this.pagesContainerWrapper = document.getElementById(wrapperId); this.pagesContainerWrapper = document.getElementById(wrapperId);
this.downloadLink = null; this.downloadLink = null;
this.movePageTo = this.movePageTo.bind(this); this.movePageTo = this.movePageTo.bind(this);
this.addPdfs = this.addPdfs.bind(this); this.addFiles = this.addFiles.bind(this);
this.addPdfsFromFiles = this.addPdfsFromFiles.bind(this); this.addFilesFromFiles = this.addFilesFromFiles.bind(this);
this.rotateElement = this.rotateElement.bind(this); this.rotateElement = this.rotateElement.bind(this);
this.rotateAll = this.rotateAll.bind(this); this.rotateAll = this.rotateAll.bind(this);
this.exportPdf = this.exportPdf.bind(this); this.exportPdf = this.exportPdf.bind(this);
this.updateFilename = this.updateFilename.bind(this); this.updateFilename = this.updateFilename.bind(this);
this.setDownloadAttribute = this.setDownloadAttribute.bind(this); this.setDownloadAttribute = this.setDownloadAttribute.bind(this);
this.preventIllegalChars = this.preventIllegalChars.bind(this); this.preventIllegalChars = this.preventIllegalChars.bind(this);
this.addImageFile = this.addImageFile.bind(this);
this.pdfAdapters = pdfAdapters; this.pdfAdapters = pdfAdapters;
this.pdfAdapters.forEach((adapter) => { this.pdfAdapters.forEach((adapter) => {
adapter.setActions({ adapter.setActions({
movePageTo: this.movePageTo, movePageTo: this.movePageTo,
addPdfs: this.addPdfs, addFiles: this.addFiles,
rotateElement: this.rotateElement, rotateElement: this.rotateElement,
updateFilename: this.updateFilename, updateFilename: this.updateFilename,
}); });
}); });
window.addPdfs = this.addPdfs; window.addFiles = this.addFiles;
window.exportPdf = this.exportPdf; window.exportPdf = this.exportPdf;
window.rotateAll = this.rotateAll; window.rotateAll = this.rotateAll;
@ -65,25 +66,30 @@ class PdfContainer {
} }
} }
addPdfs(nextSiblingElement) { addFiles(nextSiblingElement) {
var input = document.createElement("input"); var input = document.createElement("input");
input.type = "file"; input.type = "file";
input.multiple = true; input.multiple = true;
input.setAttribute("accept", "application/pdf"); input.setAttribute("accept", "application/pdf,image/*");
input.onchange = async (e) => { input.onchange = async (e) => {
const files = e.target.files; const files = e.target.files;
this.addPdfsFromFiles(files, nextSiblingElement); this.addFilesFromFiles(files, nextSiblingElement);
this.updateFilename(files ? files[0].name : ""); this.updateFilename(files ? files[0].name : "");
}; };
input.click(); input.click();
} }
async addPdfsFromFiles(files, nextSiblingElement) { async addFilesFromFiles(files, nextSiblingElement) {
this.fileName = files[0].name; this.fileName = files[0].name;
for (var i = 0; i < files.length; i++) { 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) => { 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) { async loadFile(file) {
var objectUrl = URL.createObjectURL(file); var objectUrl = URL.createObjectURL(file);
var pdfDocument = await this.toPdfLib(objectUrl); var pdfDocument = await this.toPdfLib(objectUrl);
@ -193,16 +218,47 @@ class PdfContainer {
for (var i = 0; i < pageContainers.length; i++) { for (var i = 0; i < pageContainers.length; i++) {
const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container
if (!img) continue; if (!img) continue;
let page;
if (img.doc) {
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]); const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]);
const page = pages[0]; 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; const rotation = img.style.rotate;
if (rotation) { if (rotation) {
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, "")); const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ""));
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle)); page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle));
} }
pdfDoc.addPage(page);
} }
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" }); const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" });
@ -249,6 +305,7 @@ class PdfContainer {
} }
} }
setDownloadAttribute() { setDownloadAttribute() {
this.downloadLink.setAttribute("download", this.fileName ? this.fileName : "managed.pdf"); 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; export default PdfContainer;

View file

@ -90,6 +90,25 @@ class FileDragManager {
this.updateFilename(files ? files[0].name : ""); 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; export default FileDragManager;

View file

@ -27,7 +27,7 @@
th:placeholder="#{multiTool.uploadPrompts}"> th:placeholder="#{multiTool.uploadPrompts}">
</div> </div>
<div class="mt-action-btn"> <div class="mt-action-btn">
<button class="btn btn-primary" onclick="addPdfs()"> <button class="btn btn-primary" onclick="addFiles()">
<span class="material-symbols-rounded"> <span class="material-symbols-rounded">
add add
</span> </span>
@ -52,12 +52,12 @@
<div class="multi-tool-container"> <div class="multi-tool-container">
<div class="d-flex flex-wrap" id="pages-container-wrapper"> <div class="d-flex flex-wrap" id="pages-container-wrapper">
<div id="pages-container"> <div id="pages-container">
<div class="page-container" th:each="pdf, status: ${pdfList}" <div class="page-container" th:each="file, status: ${fileList}"
th:id="'page-container-' + ${status.index}"> th:id="'page-container-' + ${status.index}">
<div class="page-number-container"> <div class="page-number-container">
<span th:text="${status.index + 1}"></span> <span th:text="${status.index + 1}"></span>
</div> </div>
<img th:src="${pdf.imageUrl}" alt="PDF Page"> <img th:src="${file.imageUrl}" alt="File Page">
</div> </div>
</div> </div>
</div> </div>
@ -82,14 +82,14 @@
const dragDropManager = new DragDropManager('drag-container', 'pages-container'); const dragDropManager = new DragDropManager('drag-container', 'pages-container');
// enables image highlight on click // enables image highlight on click
const imageHighlighter = new ImageHighlighter('image-highlighter'); const imageHighlighter = new ImageHighlighter('image-highlighter');
// enables the default action buttons on each pdf // enables the default action buttons on each file
const pdfActionsManager = new PdfActionsManager('pages-container'); const pdfActionsManager = new PdfActionsManager('pages-container');
const fileDragManager = new FileDragManager(); const fileDragManager = new FileDragManager();
// Scroll the wrapper horizontally // Scroll the wrapper horizontally
scrollDivHorizontally('pages-container-wrapper'); scrollDivHorizontally('pages-container-wrapper');
// Automatically exposes rotateAll, addPdfs and exportPdf to the window for the global buttons. // Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
const pdfContainer = new PdfContainer( const pdfContainer = new PdfContainer(
'pages-container', 'pages-container',
'pages-container-wrapper', 'pages-container-wrapper',
@ -101,7 +101,7 @@
] ]
) )
fileDragManager.setCallback(async (files) => pdfContainer.addPdfsFromFiles(files)); fileDragManager.setCallback(async (files) => pdfContainer.addFilesFromFiles(files));
</script> </script>
</body> </body>