rearrange support n numbers, downloader.js fixes

This commit is contained in:
Anthony Stirling 2023-06-03 17:21:59 +01:00
parent e2a787e519
commit 4594765cbd
4 changed files with 235 additions and 245 deletions

View file

@ -6,6 +6,11 @@ import stirling.software.SPDF.utils.WebResponseUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
@ -24,228 +29,241 @@ import io.swagger.v3.oas.annotations.Parameter;
@RestController @RestController
public class RearrangePagesPDFController { 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<byte[]> 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") PDDocument document = PDDocument.load(pdfFile.getBytes());
@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<byte[]> 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()); // 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 List<Integer> pagesToRemove = pageOrderToString(pageOrderArr, document.getNumberOfPages());
String[] pageOrderArr = pagesToDelete.split(",");
List<Integer> 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<Integer> pageOrderToString(String[] pageOrderArr, int totalPages) { private List<Integer> removeFirst(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>(); if (totalPages <= 1)
// loop through the page order array return new ArrayList<>();
for (String element : pageOrderArr) { List<Integer> newPageOrder = new ArrayList<>();
// check if the element contains a range of pages for (int i = 2; i <= totalPages; i++) {
if (element.contains("-")) { newPageOrder.add(i - 1);
// split the range into start and end page }
String[] range = element.split("-"); return newPageOrder;
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; private List<Integer> removeLast(int totalPages) {
} if (totalPages <= 1)
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i < totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private enum CustomMode { private List<Integer> removeFirstAndLast(int totalPages) {
REVERSE_ORDER, if (totalPages <= 2)
DUPLEX_SORT, return new ArrayList<>();
BOOKLET_SORT, List<Integer> newPageOrder = new ArrayList<>();
ODD_EVEN_SPLIT, for (int i = 2; i < totalPages; i++) {
REMOVE_FIRST, newPageOrder.add(i - 1);
REMOVE_LAST, }
REMOVE_FIRST_AND_LAST, return newPageOrder;
} }
private List<Integer> removeFirst(int totalPages) { private List<Integer> reverseOrder(int totalPages) {
if (totalPages <= 1) return new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); for (int i = totalPages; i >= 1; i--) {
for (int i = 2; i <= totalPages; i++) { newPageOrder.add(i - 1);
newPageOrder.add(i - 1); }
} return newPageOrder;
return newPageOrder; }
}
private List<Integer> removeLast(int totalPages) { private List<Integer> duplexSort(int totalPages) {
if (totalPages <= 1) return new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
for (int i = 1; i < totalPages; i++) { for (int i = 1; i <= half; i++) {
newPageOrder.add(i - 1); newPageOrder.add(i - 1);
} if (i <= totalPages - half) { // Avoid going out of bounds
return newPageOrder; newPageOrder.add(totalPages - i);
} }
}
return newPageOrder;
}
private List<Integer> removeFirstAndLast(int totalPages) { private List<Integer> bookletSort(int totalPages) {
if (totalPages <= 2) return new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); for (int i = 0; i < totalPages / 2; i++) {
for (int i = 2; i < totalPages; i++) { newPageOrder.add(i);
newPageOrder.add(i - 1); newPageOrder.add(totalPages - i - 1);
} }
return newPageOrder; return newPageOrder;
} }
private List<Integer> oddEvenSplit(int totalPages) {
private List<Integer> reverseOrder(int totalPages) { List<Integer> newPageOrder = new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); for (int i = 1; i <= totalPages; i += 2) {
for (int i = totalPages; i >= 1; i--) { newPageOrder.add(i - 1);
newPageOrder.add(i - 1); }
} for (int i = 2; i <= totalPages; i += 2) {
return newPageOrder; newPageOrder.add(i - 1);
} }
return newPageOrder;
}
private List<Integer> duplexSort(int totalPages) { private List<Integer> processCustomMode(String customMode, int totalPages) {
List<Integer> newPageOrder = new ArrayList<>(); try {
int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages CustomMode mode = CustomMode.valueOf(customMode.toUpperCase());
for (int i = 1; i <= half; i++) { switch (mode) {
newPageOrder.add(i - 1); case REVERSE_ORDER:
if (i <= totalPages - half) { // Avoid going out of bounds return reverseOrder(totalPages);
newPageOrder.add(totalPages - i); case DUPLEX_SORT:
} return duplexSort(totalPages);
} case BOOKLET_SORT:
return newPageOrder; 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<byte[]> 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<Integer> bookletSort(int totalPages) { // Split the page order string into an array of page numbers or range of numbers
List<Integer> newPageOrder = new ArrayList<>(); String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
for (int i = 0; i < totalPages / 2; i++) { int totalPages = document.getNumberOfPages();
newPageOrder.add(i); System.out.println("pageOrder=" + pageOrder);
newPageOrder.add(totalPages - i - 1); System.out.println("customMode length =" + customMode.length());
} List<Integer> newPageOrder;
return newPageOrder; if (customMode != null && customMode.length() > 0) {
} newPageOrder = processCustomMode(customMode, totalPages);
} else {
newPageOrder = pageOrderToString(pageOrderArr, totalPages);
}
private List<Integer> oddEvenSplit(int totalPages) { // Create a new list to hold the pages in the new order
List<Integer> newPageOrder = new ArrayList<>(); List<PDPage> newPages = new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) { for (int i = 0; i < newPageOrder.size(); i++) {
newPageOrder.add(i - 1); newPages.add(document.getPage(newPageOrder.get(i)));
} }
for (int i = 2; i <= totalPages; i += 2) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> processCustomMode(String customMode, int totalPages) { // Remove all the pages from the original document
try { for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
CustomMode mode = CustomMode.valueOf(customMode.toUpperCase()); document.removePage(i);
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") // Add the pages in the new order
@Operation(summary = "Rearrange pages in a PDF file", for (PDPage page : newPages) {
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.") document.addPage(page);
public ResponseEntity<byte[]> 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());
// Split the page order string into an array of page numbers or range of numbers return WebResponseUtils.pdfDocToWebResponse(document,
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf");
int totalPages = document.getNumberOfPages(); } catch (IOException e) {
System.out.println("pageOrder=" + pageOrder); logger.error("Failed rearranging documents", e);
System.out.println("customMode length =" + customMode.length()); return null;
List<Integer> newPageOrder; }
if(customMode != null && customMode.length() > 0) { }
newPageOrder = processCustomMode(customMode, totalPages);
} else {
newPageOrder = pageOrderToString(pageOrderArr, totalPages);
}
// Create a new list to hold the pages in the new order private List<Integer> pageOrderToString(String[] pageOrderArr, int totalPages) {
List<PDPage> newPages = new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {
newPages.add(document.getPage(newPageOrder.get(i)));
}
// Remove all the pages from the original document // loop through the page order array
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { for (String element : pageOrderArr) {
document.removePage(i); // 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 if (element.contains("n")) {
for (PDPage page : newPages) { String[] parts = element.split("n");
document.addPage(page); 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"); for (int i = 1; i <= totalPages; i++) {
} catch (IOException e) { int pageNum = coefficientExists ? coefficient * i : i;
logger.error("Failed rearranging documents", e); pageNum += constantExists ? constant : 0;
return null;
}
}
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;
}
} }

View file

@ -10,7 +10,7 @@ multiPdfDropPrompt=Select (or drag & drop) all PDFs you require
imgPrompt=Select Image(s) imgPrompt=Select Image(s)
genericSubmit=Submit genericSubmit=Submit
processTimeWarning=Warning: This process can take up to a minute depending on file-size 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 goToPage=Go
true=True true=True
false=False false=False

View file

@ -19,25 +19,9 @@ $(document).ready(function() {
try { try {
if (override === 'multi' || files.length > 1 && override !== 'single') { 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); await submitMultiPdfForm(url, files);
} else { } else {
const downloadDetails = await handleSingleDownload(url, formData); 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);
} }
$('#submitBtn').text('Submit'); $('#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 { try {
const response = await fetch(url, { method: 'POST', body: formData }); const response = await fetch(url, { method: 'POST', body: formData });
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
@ -90,7 +54,7 @@ async function handleSingleDownload(url, formData) {
const blob = await response.blob(); const blob = await response.blob();
if (contentType.includes('application/pdf') || contentType.includes('image/')) { if (contentType.includes('application/pdf') || contentType.includes('image/')) {
return handleResponse(blob, filename, true); return handleResponse(blob, filename, !isMulti);
} else { } else {
return handleResponse(blob, filename); return handleResponse(blob, filename);
} }
@ -118,7 +82,7 @@ function getFilenameFromContentDisposition(contentDisposition) {
async function handleJsonResponse(response) { async function handleJsonResponse(response) {
const json = await response.json(); const json = await response.json();
const errorMessage = JSON.stringify(json, null, 2); 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}]]'); alert('[[#{error.pdfPassword}]]');
} else { } else {
showErrorBanner(json.error + ':' + json.message, json.trace); 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 zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
const zipFiles = files.length > zipThreshold; const zipFiles = files.length > zipThreshold;
let jszip = null; let jszip = null;
//let progressBar = $('#progressBar'); // Show the progress bar
//progressBar.css('width', '0%'); $('#progressBarContainer').show();
//progressBar.attr('aria-valuenow', 0); // Initialize the progress bar
//progressBar.attr('aria-valuemax', Array.from(files).length);
let progressBar = $('#progressBar');
progressBar.css('width', '0%');
progressBar.attr('aria-valuenow', 0);
progressBar.attr('aria-valuemax', files.length);
if (zipFiles) { if (zipFiles) {
jszip = new JSZip(); jszip = new JSZip();
} }
@ -202,20 +171,21 @@ async function submitMultiPdfForm(url, files) {
} }
try { try {
const downloadDetails = await handleSingleDownload(url, fileFormData); const downloadDetails = await handleSingleDownload(url, fileFormData, true);
console.log(downloadDetails); console.log(downloadDetails);
if (zipFiles) { if (zipFiles) {
jszip.file(downloadDetails.filename, downloadDetails.blob); jszip.file(downloadDetails.filename, downloadDetails.blob);
} else { } else {
downloadFile(downloadDetails.blob, downloadDetails.filename); downloadFile(downloadDetails.blob, downloadDetails.filename);
} }
//updateProgressBar(progressBar, Array.from(files).length); updateProgressBar(progressBar, Array.from(files).length);
} catch (error) { } catch (error) {
handleDownloadError(error); handleDownloadError(error);
console.error(error); console.error(error);
} }
}); });
await Promise.all(promises); await Promise.all(promises);
} }
if (zipFiles) { if (zipFiles) {
@ -226,6 +196,8 @@ async function submitMultiPdfForm(url, files) {
console.error('Error generating ZIP file: ' + error); console.error('Error generating ZIP file: ' + error);
} }
} }
progressBar.css('width', '100%');
progressBar.attr('aria-valuenow', Array.from(files).length);
} }

View file

@ -31,7 +31,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label> <label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageOrder" placeholder="(e.g. 1,3,2 or 4-8,2,10-12)" required> <input type="text" class="form-control" id="pageOrder" name="pageOrder" placeholder="(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)" required>
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button>