diff --git a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index 718f8520..8ae1b533 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -6,6 +6,11 @@ import stirling.software.SPDF.utils.WebResponseUtils; import java.util.ArrayList; import java.util.List; +import javax.script.ScriptEngineManager; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import javax.script.ScriptEngine; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -24,228 +29,241 @@ import io.swagger.v3.oas.annotations.Parameter; @RestController public class RearrangePagesPDFController { - private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); + private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); + @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") + @Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete.") + public ResponseEntity deletePages( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile, + @RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete) + throws IOException { - @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") - @Operation(summary = "Remove pages from a PDF file", - description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete.") - public ResponseEntity deletePages( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF file from which pages will be removed") - MultipartFile pdfFile, - @RequestParam("pagesToDelete") - @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") - String pagesToDelete) throws IOException { + PDDocument document = PDDocument.load(pdfFile.getBytes()); - PDDocument document = PDDocument.load(pdfFile.getBytes()); + // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pagesToDelete.split(","); - // Split the page order string into an array of page numbers or range of numbers - String[] pageOrderArr = pagesToDelete.split(","); + List pagesToRemove = pageOrderToString(pageOrderArr, document.getNumberOfPages()); - List pagesToRemove = pageOrderToString(pageOrderArr, document.getNumberOfPages()); + for (int i = pagesToRemove.size() - 1; i >= 0; i--) { + int pageIndex = pagesToRemove.get(i); + document.removePage(pageIndex); + } + return WebResponseUtils.pdfDocToWebResponse(document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); - for (int i = pagesToRemove.size() - 1; i >= 0; i--) { - int pageIndex = pagesToRemove.get(i); - document.removePage(pageIndex); - } - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); + } - } + private enum CustomMode { + REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST, + } - private List pageOrderToString(String[] pageOrderArr, int totalPages) { - List newPageOrder = new ArrayList<>(); - // loop through the page order array - for (String element : pageOrderArr) { - // check if the element contains a range of pages - if (element.contains("-")) { - // split the range into start and end page - String[] range = element.split("-"); - int start = Integer.parseInt(range[0]); - int end = Integer.parseInt(range[1]); - // check if the end page is greater than total pages - if (end > totalPages) { - end = totalPages; - } - // loop through the range of pages - for (int j = start; j <= end; j++) { - // print the current index - newPageOrder.add(j - 1); - } - } else { - // if the element is a single page - newPageOrder.add(Integer.parseInt(element) - 1); - } - } + private List removeFirst(int totalPages) { + if (totalPages <= 1) + return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 2; i <= totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - return newPageOrder; - } + private List removeLast(int totalPages) { + if (totalPages <= 1) + return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 1; i < totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - private enum CustomMode { - REVERSE_ORDER, - DUPLEX_SORT, - BOOKLET_SORT, - ODD_EVEN_SPLIT, - REMOVE_FIRST, - REMOVE_LAST, - REMOVE_FIRST_AND_LAST, - } + private List removeFirstAndLast(int totalPages) { + if (totalPages <= 2) + return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 2; i < totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - private List removeFirst(int totalPages) { - if (totalPages <= 1) return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 2; i <= totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + private List reverseOrder(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = totalPages; i >= 1; i--) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - private List removeLast(int totalPages) { - if (totalPages <= 1) return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 1; i < totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + private List duplexSort(int totalPages) { + List newPageOrder = new ArrayList<>(); + int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages + for (int i = 1; i <= half; i++) { + newPageOrder.add(i - 1); + if (i <= totalPages - half) { // Avoid going out of bounds + newPageOrder.add(totalPages - i); + } + } + return newPageOrder; + } - private List removeFirstAndLast(int totalPages) { - if (totalPages <= 2) return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 2; i < totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + private List bookletSort(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 0; i < totalPages / 2; i++) { + newPageOrder.add(i); + newPageOrder.add(totalPages - i - 1); + } + return newPageOrder; + } - - private List reverseOrder(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = totalPages; i >= 1; i--) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + private List oddEvenSplit(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 1; i <= totalPages; i += 2) { + newPageOrder.add(i - 1); + } + for (int i = 2; i <= totalPages; i += 2) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - private List duplexSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages - for (int i = 1; i <= half; i++) { - newPageOrder.add(i - 1); - if (i <= totalPages - half) { // Avoid going out of bounds - newPageOrder.add(totalPages - i); - } - } - return newPageOrder; - } + private List processCustomMode(String customMode, int totalPages) { + try { + CustomMode mode = CustomMode.valueOf(customMode.toUpperCase()); + switch (mode) { + case REVERSE_ORDER: + return reverseOrder(totalPages); + case DUPLEX_SORT: + return duplexSort(totalPages); + case BOOKLET_SORT: + return bookletSort(totalPages); + case ODD_EVEN_SPLIT: + return oddEvenSplit(totalPages); + case REMOVE_FIRST: + return removeFirst(totalPages); + case REMOVE_LAST: + return removeLast(totalPages); + case REMOVE_FIRST_AND_LAST: + return removeFirstAndLast(totalPages); + default: + throw new IllegalArgumentException("Unsupported custom mode"); + } + } catch (IllegalArgumentException e) { + logger.error("Unsupported custom mode", e); + return null; + } + } + @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") + @Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode.") + public ResponseEntity rearrangePages( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to rearrange pages") MultipartFile pdfFile, + @RequestParam(required = false, value = "pageOrder") @Parameter(description = "The new page order as a comma-separated list of page numbers, page ranges (e.g., '1,3,5-7'), or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')") String pageOrder, + @RequestParam(required = false, value = "customMode") @Parameter(schema = @Schema(implementation = CustomMode.class, description = "The custom mode for page rearrangement. " + + "Valid values are:\n" + "REVERSE_ORDER: Reverses the order of all pages.\n" + + "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). " + + "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n" + + "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n" + + "REMOVE_FIRST: Removes the first page.\n" + "REMOVE_LAST: Removes the last page.\n" + + "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")) String customMode) { + try { + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); - private List bookletSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < totalPages / 2; i++) { - newPageOrder.add(i); - newPageOrder.add(totalPages - i - 1); - } - return newPageOrder; - } + // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; + int totalPages = document.getNumberOfPages(); + System.out.println("pageOrder=" + pageOrder); + System.out.println("customMode length =" + customMode.length()); + List newPageOrder; + if (customMode != null && customMode.length() > 0) { + newPageOrder = processCustomMode(customMode, totalPages); + } else { + newPageOrder = pageOrderToString(pageOrderArr, totalPages); + } - private List oddEvenSplit(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 1; i <= totalPages; i += 2) { - newPageOrder.add(i - 1); - } - for (int i = 2; i <= totalPages; i += 2) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + // Create a new list to hold the pages in the new order + List newPages = new ArrayList<>(); + for (int i = 0; i < newPageOrder.size(); i++) { + newPages.add(document.getPage(newPageOrder.get(i))); + } - private List processCustomMode(String customMode, int totalPages) { - try { - CustomMode mode = CustomMode.valueOf(customMode.toUpperCase()); - switch (mode) { - case REVERSE_ORDER: - return reverseOrder(totalPages); - case DUPLEX_SORT: - return duplexSort(totalPages); - case BOOKLET_SORT: - return bookletSort(totalPages); - case ODD_EVEN_SPLIT: - return oddEvenSplit(totalPages); - case REMOVE_FIRST: - return removeFirst(totalPages); - case REMOVE_LAST: - return removeLast(totalPages); - case REMOVE_FIRST_AND_LAST: - return removeFirstAndLast(totalPages); - default: - throw new IllegalArgumentException("Unsupported custom mode"); - } - } catch (IllegalArgumentException e) { - logger.error("Unsupported custom mode", e); - return null; - } - } + // Remove all the pages from the original document + for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { + document.removePage(i); + } - @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") - @Operation(summary = "Rearrange pages in a PDF file", - description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode.") - public ResponseEntity rearrangePages( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF file to rearrange pages") - MultipartFile pdfFile, - @RequestParam(required = false, value = "pageOrder") - @Parameter(description = "The new page order as a comma-separated list of page numbers or page ranges (e.g., '1,3,5-7')") - String pageOrder, - @RequestParam(required = false, value = "customMode") - @Parameter(schema = @Schema(implementation = CustomMode.class, description = "The custom mode for page rearrangement. " + - "Valid values are:\n" + - "REVERSE_ORDER: Reverses the order of all pages.\n" + - "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). " + - "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n" + - "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n" + - "REMOVE_FIRST: Removes the first page.\n" + - "REMOVE_LAST: Removes the last page.\n" + - "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")) - String customMode) { - try { - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); + // Add the pages in the new order + for (PDPage page : newPages) { + document.addPage(page); + } - // Split the page order string into an array of page numbers or range of numbers - String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; - int totalPages = document.getNumberOfPages(); - System.out.println("pageOrder=" + pageOrder); - System.out.println("customMode length =" + customMode.length()); - List newPageOrder; - if(customMode != null && customMode.length() > 0) { - newPageOrder = processCustomMode(customMode, totalPages); - } else { - newPageOrder = pageOrderToString(pageOrderArr, totalPages); - } + return WebResponseUtils.pdfDocToWebResponse(document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); + } catch (IOException e) { + logger.error("Failed rearranging documents", e); + return null; + } + } - // Create a new list to hold the pages in the new order - List newPages = new ArrayList<>(); - for (int i = 0; i < newPageOrder.size(); i++) { - newPages.add(document.getPage(newPageOrder.get(i))); - } + private List pageOrderToString(String[] pageOrderArr, int totalPages) { + List newPageOrder = new ArrayList<>(); - // Remove all the pages from the original document - for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { - document.removePage(i); - } + // loop through the page order array + for (String element : pageOrderArr) { + // check if the element contains a range of pages + if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { + // Handle page order as a function + int coefficient = 0; + int constant = 0; + boolean coefficientExists = false; + boolean constantExists = false; - // Add the pages in the new order - for (PDPage page : newPages) { - document.addPage(page); - } + if (element.contains("n")) { + String[] parts = element.split("n"); + if (!parts[0].equals("") && parts[0] != null) { + coefficient = Integer.parseInt(parts[0]); + coefficientExists = true; + } + if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { + constant = Integer.parseInt(parts[1]); + constantExists = true; + } + } else if (element.contains("+")) { + constant = Integer.parseInt(element.replace("+", "")); + constantExists = true; + } - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); - } catch (IOException e) { - logger.error("Failed rearranging documents", e); - return null; - } - } + for (int i = 1; i <= totalPages; i++) { + int pageNum = coefficientExists ? coefficient * i : i; + pageNum += constantExists ? constant : 0; + if (pageNum <= totalPages && pageNum > 0) { + newPageOrder.add(pageNum - 1); + } + } + } else if (element.contains("-")) { + // split the range into start and end page + String[] range = element.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + // check if the end page is greater than total pages + if (end > totalPages) { + end = totalPages; + } + // loop through the range of pages + for (int j = start; j <= end; j++) { + // print the current index + newPageOrder.add(j - 1); + } + } else { + // if the element is a single page + newPageOrder.add(Integer.parseInt(element) - 1); + } + } + + return newPageOrder; + } } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 5eee2506..04f7df53 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -10,7 +10,7 @@ multiPdfDropPrompt=Select (or drag & drop) all PDFs you require imgPrompt=Select Image(s) genericSubmit=Submit processTimeWarning=Warning: This process can take up to a minute depending on file-size -pageOrderPrompt=Custom Page Order (Enter a comma-separated list of page numbers) : +pageOrderPrompt=Custom Page Order (Enter a comma-separated list of page numbers or Functions like 2n+1) : goToPage=Go true=True false=False diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js index 5dc1912f..35623db6 100644 --- a/src/main/resources/static/js/downloader.js +++ b/src/main/resources/static/js/downloader.js @@ -19,25 +19,9 @@ $(document).ready(function() { try { if (override === 'multi' || files.length > 1 && override !== 'single') { - // Show the progress bar - $('#progressBarContainer').show(); - // Initialize the progress bar - //let progressBar = $('#progressBar'); - //progressBar.css('width', '0%'); - //progressBar.attr('aria-valuenow', 0); - //progressBar.attr('aria-valuemax', files.length); - await submitMultiPdfForm(url, files); } else { - const downloadDetails = await handleSingleDownload(url, formData); - const downloadOption = localStorage.getItem('downloadOption'); - - // Handle the download action according to the selected option - //handleDownloadAction(downloadOption, downloadDetails.blob, downloadDetails.filename); - - // Update the progress bar - //updateProgressBar(progressBar, 1); - + await handleSingleDownload(url, formData); } $('#submitBtn').text('Submit'); @@ -49,29 +33,9 @@ $(document).ready(function() { }); }); -function handleDownloadAction(downloadOption, blob, filename) { - const url = URL.createObjectURL(blob); - switch (downloadOption) { - case 'sameWindow': - // Open the file in the same window - window.location.href = url; - break; - case 'newWindow': - // Open the file in a new window - window.open(url, '_blank'); - break; - default: - // Download the file - const link = document.createElement('a'); - link.href = url; - link.download = filename; - link.click(); - break; - } -} -async function handleSingleDownload(url, formData) { +async function handleSingleDownload(url, formData, isMulti = false) { try { const response = await fetch(url, { method: 'POST', body: formData }); const contentType = response.headers.get('content-type'); @@ -90,7 +54,7 @@ async function handleSingleDownload(url, formData) { const blob = await response.blob(); if (contentType.includes('application/pdf') || contentType.includes('image/')) { - return handleResponse(blob, filename, true); + return handleResponse(blob, filename, !isMulti); } else { return handleResponse(blob, filename); } @@ -118,7 +82,7 @@ function getFilenameFromContentDisposition(contentDisposition) { async function handleJsonResponse(response) { const json = await response.json(); const errorMessage = JSON.stringify(json, null, 2); - if (errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided')) { + if (errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided') || errorMessage.toLowerCase().includes('PDF contains an encryption dictionary')) { alert('[[#{error.pdfPassword}]]'); } else { showErrorBanner(json.error + ':' + json.message, json.trace); @@ -173,10 +137,15 @@ async function submitMultiPdfForm(url, files) { const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4; const zipFiles = files.length > zipThreshold; let jszip = null; - //let progressBar = $('#progressBar'); - //progressBar.css('width', '0%'); - //progressBar.attr('aria-valuenow', 0); - //progressBar.attr('aria-valuemax', Array.from(files).length); + // Show the progress bar + $('#progressBarContainer').show(); + // Initialize the progress bar + + let progressBar = $('#progressBar'); + progressBar.css('width', '0%'); + progressBar.attr('aria-valuenow', 0); + progressBar.attr('aria-valuemax', files.length); + if (zipFiles) { jszip = new JSZip(); } @@ -202,20 +171,21 @@ async function submitMultiPdfForm(url, files) { } try { - const downloadDetails = await handleSingleDownload(url, fileFormData); + const downloadDetails = await handleSingleDownload(url, fileFormData, true); console.log(downloadDetails); if (zipFiles) { jszip.file(downloadDetails.filename, downloadDetails.blob); } else { downloadFile(downloadDetails.blob, downloadDetails.filename); } - //updateProgressBar(progressBar, Array.from(files).length); + updateProgressBar(progressBar, Array.from(files).length); } catch (error) { handleDownloadError(error); console.error(error); } }); await Promise.all(promises); + } if (zipFiles) { @@ -226,6 +196,8 @@ async function submitMultiPdfForm(url, files) { console.error('Error generating ZIP file: ' + error); } } + progressBar.css('width', '100%'); + progressBar.attr('aria-valuenow', Array.from(files).length); } diff --git a/src/main/resources/templates/pdf-organizer.html b/src/main/resources/templates/pdf-organizer.html index 580f88a0..6accc45f 100644 --- a/src/main/resources/templates/pdf-organizer.html +++ b/src/main/resources/templates/pdf-organizer.html @@ -31,7 +31,7 @@
- +