diff --git a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 7639d154..035a1214 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -1,7 +1,13 @@ package stirling.software.SPDF.controller.api; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; @@ -11,10 +17,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; - +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,55 +33,93 @@ public class MergeController { private static final Logger logger = LoggerFactory.getLogger(MergeController.class); - private PDDocument mergeDocuments(List documents) throws IOException { - // Create a new empty document - PDDocument mergedDoc = new PDDocument(); - // Iterate over the list of documents and add their pages to the merged document - for (PDDocument doc : documents) { - // Get all pages from the current document - PDPageTree pages = doc.getPages(); - // Iterate over the pages and add them to the merged document - for (PDPage page : pages) { - mergedDoc.addPage(page); - } - - +private PDDocument mergeDocuments(List documents) throws IOException { + PDDocument mergedDoc = new PDDocument(); + for (PDDocument doc : documents) { + for (PDPage page : doc.getPages()) { + mergedDoc.addPage(page); } + } + return mergedDoc; +} - // Return the merged document - return mergedDoc; +private Comparator getSortComparator(String sortType) { + switch (sortType) { + case "byFileName": + return Comparator.comparing(MultipartFile::getOriginalFilename); + case "byDateModified": + return (file1, file2) -> { + try { + BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); + } catch (IOException e) { + return 0; // If there's an error, treat them as equal + } + }; + case "byDateCreated": + return (file1, file2) -> { + try { + BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + return attr1.creationTime().compareTo(attr2.creationTime()); + } catch (IOException e) { + return 0; // If there's an error, treat them as equal + } + }; + case "byPDFTitle": + return (file1, file2) -> { + try (PDDocument doc1 = PDDocument.load(file1.getInputStream()); + PDDocument doc2 = PDDocument.load(file2.getInputStream())) { + String title1 = doc1.getDocumentInformation().getTitle(); + String title2 = doc2.getDocumentInformation().getTitle(); + return title1.compareTo(title2); + } catch (IOException e) { + return 0; + } + }; + case "orderProvided": + default: + return (file1, file2) -> 0; // Default is the order provided + } +} + +@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") +@Operation(summary = "Merge multiple PDF files into one", + description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") +public ResponseEntity mergePdfs( + @RequestPart(required = true, value = "fileInput") MultipartFile[] files, + @RequestParam(value = "sortType", defaultValue = "orderProvided") + @Parameter(schema = @Schema(description = "The type of sorting to be applied on the input files before merging.", + allowableValues = { + "orderProvided", + "byFileName", + "byDateModified", + "byDateCreated", + "byPDFTitle" + })) + String sortType) throws IOException { + + Arrays.sort(files, getSortComparator(sortType)); + + List documents = new ArrayList<>(); + for (MultipartFile file : files) { + try (InputStream is = file.getInputStream()) { + documents.add(PDDocument.load(is)); + } } - @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") - @Operation( - summary = "Merge multiple PDF files into one", - description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO" - ) - public ResponseEntity mergePdfs( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF files to be merged into a single file", required = true) - MultipartFile[] files) throws IOException { - // Read the input PDF files into PDDocument objects - List documents = new ArrayList<>(); - - // Loop through the files array and read each file into a PDDocument - for (MultipartFile file : files) { - documents.add(PDDocument.load(file.getInputStream())); - } - - PDDocument mergedDoc = mergeDocuments(documents); - - - // Return the merged PDF as a response + try (PDDocument mergedDoc = mergeDocuments(documents)) { ResponseEntity response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); - - for (PDDocument doc : documents) { - // Close the document after processing - doc.close(); - } - return response; + } finally { + for (PDDocument doc : documents) { + if (doc != null) { + doc.close(); + } + } } +} } \ No newline at end of file diff --git a/src/main/resources/static/js/merge.js b/src/main/resources/static/js/merge.js index 523be4a8..d27730b9 100644 --- a/src/main/resources/static/js/merge.js +++ b/src/main/resources/static/js/merge.js @@ -1,63 +1,113 @@ +let currentSort = { + field: null, + descending: false +}; + document.getElementById("fileInput-input").addEventListener("change", function() { - var files = this.files; - var list = document.getElementById("selectedFiles"); - list.innerHTML = ""; - for (var i = 0; i < files.length; i++) { - var item = document.createElement("li"); - item.className = "list-group-item"; - item.innerHTML = ` -
-
${files[i].name}
-
- - -
-
- `; - list.appendChild(item); - } + var files = this.files; + displayFiles(files); +}); - var moveUpButtons = document.querySelectorAll(".move-up"); - for (var i = 0; i < moveUpButtons.length; i++) { - moveUpButtons[i].addEventListener("click", function(event) { - event.preventDefault(); - var parent = this.closest(".list-group-item"); - var grandParent = parent.parentNode; - if (parent.previousElementSibling) { - grandParent.insertBefore(parent, parent.previousElementSibling); - updateFiles(); - } - }); - } +function displayFiles(files) { + var list = document.getElementById("selectedFiles"); + list.innerHTML = ""; - var moveDownButtons = document.querySelectorAll(".move-down"); - for (var i = 0; i < moveDownButtons.length; i++) { - moveDownButtons[i].addEventListener("click", function(event) { - event.preventDefault(); - var parent = this.closest(".list-group-item"); - var grandParent = parent.parentNode; - if (parent.nextElementSibling) { - grandParent.insertBefore(parent.nextElementSibling, parent); - updateFiles(); - } - }); - } + for (var i = 0; i < files.length; i++) { + var item = document.createElement("li"); + item.className = "list-group-item"; + item.innerHTML = ` +
+
${files[i].name}
+
+ + +
+
+ `; + list.appendChild(item); + } - function updateFiles() { - var dataTransfer = new DataTransfer(); - var liElements = document.querySelectorAll("#selectedFiles li"); + attachMoveButtons(); +} - for (var i = 0; i < liElements.length; i++) { - var fileNameFromList = liElements[i].querySelector(".filename").innerText; - var fileFromFiles; - for (var j = 0; j < files.length; j++) { - var file = files[j]; - if (file.name === fileNameFromList) { - dataTransfer.items.add(file); - break; - } - } - } - document.getElementById("fileInput-input").files = dataTransfer.files; - } -}); \ No newline at end of file +function attachMoveButtons() { + var moveUpButtons = document.querySelectorAll(".move-up"); + for (var i = 0; i < moveUpButtons.length; i++) { + moveUpButtons[i].addEventListener("click", function(event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + var grandParent = parent.parentNode; + if (parent.previousElementSibling) { + grandParent.insertBefore(parent, parent.previousElementSibling); + updateFiles(); + } + }); + } + + var moveDownButtons = document.querySelectorAll(".move-down"); + for (var i = 0; i < moveDownButtons.length; i++) { + moveDownButtons[i].addEventListener("click", function(event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + var grandParent = parent.parentNode; + if (parent.nextElementSibling) { + grandParent.insertBefore(parent.nextElementSibling, parent); + updateFiles(); + } + }); + } +} + +document.getElementById("sortByNameBtn").addEventListener("click", function() { + if (currentSort.field === "name" && !currentSort.descending) { + currentSort.descending = true; + sortFiles((a, b) => b.name.localeCompare(a.name)); + } else { + currentSort.field = "name"; + currentSort.descending = false; + sortFiles((a, b) => a.name.localeCompare(b.name)); + } +}); + +document.getElementById("sortByDateBtn").addEventListener("click", function() { + if (currentSort.field === "lastModified" && !currentSort.descending) { + currentSort.descending = true; + sortFiles((a, b) => b.lastModified - a.lastModified); + } else { + currentSort.field = "lastModified"; + currentSort.descending = false; + sortFiles((a, b) => a.lastModified - b.lastModified); + } +}); + +function sortFiles(comparator) { + // Convert FileList to array and sort + const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator); + + // Refresh displayed list + displayFiles(sortedFilesArray); + + // Update the files property + const dataTransfer = new DataTransfer(); + sortedFilesArray.forEach(file => dataTransfer.items.add(file)); + document.getElementById("fileInput-input").files = dataTransfer.files; +} + +function updateFiles() { + var dataTransfer = new DataTransfer(); + var liElements = document.querySelectorAll("#selectedFiles li"); + const files = document.getElementById("fileInput-input").files; + + for (var i = 0; i < liElements.length; i++) { + var fileNameFromList = liElements[i].querySelector(".filename").innerText; + var fileFromFiles; + for (var j = 0; j < files.length; j++) { + var file = files[j]; + if (file.name === fileNameFromList) { + dataTransfer.items.add(file); + break; + } + } + } + document.getElementById("fileInput-input").files = dataTransfer.files; +} diff --git a/src/main/resources/templates/merge-pdfs.html b/src/main/resources/templates/merge-pdfs.html index 0574f3ad..72be72e7 100644 --- a/src/main/resources/templates/merge-pdfs.html +++ b/src/main/resources/templates/merge-pdfs.html @@ -23,6 +23,8 @@
    + +