Merge branch 'main' into dependabot/gradle/org.springframework.boot-spring-boot-starter-web-3.1.0

This commit is contained in:
Anthony Stirling 2023-06-01 10:48:04 +01:00 committed by GitHub
commit 602df08df5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 733 additions and 400 deletions

View file

@ -65,6 +65,7 @@ public class EndpointConfiguration {
addEndpointToGroup("PageOps", "pdf-organizer"); addEndpointToGroup("PageOps", "pdf-organizer");
addEndpointToGroup("PageOps", "rotate-pdf"); addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout"); addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "scale-pages");
// Adding endpoints to "Convert" group // Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "pdf-to-img");
@ -164,7 +165,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "change-metadata"); addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Java", "cert-sign"); addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "multi-page-layout"); addEndpointToGroup("Java", "multi-page-layout");
addEndpointToGroup("Java", "scale-pages");
//Javascript //Javascript

View file

@ -17,7 +17,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class MergeController { public class MergeController {
@ -65,7 +65,7 @@ public class MergeController {
// Return the merged PDF as a response // Return the merged PDF as a response
ResponseEntity<byte[]> response = PdfUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
for (PDDocument doc : documents) { for (PDDocument doc : documents) {
// Close the document after processing // Close the document after processing

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -6,7 +6,6 @@ import java.io.IOException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -25,6 +24,7 @@ import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class MultiPageLayoutController { public class MultiPageLayoutController {
@ -92,9 +92,8 @@ public class MultiPageLayoutController {
outputPdf.close(); outputPdf.close();
byte[] pdfContent = baos.toByteArray(); byte[] pdfContent = baos.toByteArray();
pdfDoc.close(); pdfDoc.close();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"modifiedDocument.pdf\"") return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
.body(pdfContent);
} }
} }

View file

@ -2,6 +2,8 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.WebResponseUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -18,7 +20,6 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils;
@RestController @RestController
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
@ -48,7 +49,7 @@ public class RearrangePagesPDFController {
int pageIndex = pagesToRemove.get(i); int pageIndex = pagesToRemove.get(i);
document.removePage(pageIndex); document.removePage(pageIndex);
} }
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
} }
@ -239,7 +240,7 @@ public class RearrangePagesPDFController {
document.addPage(page); document.addPage(page);
} }
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed rearranging documents", e); logger.error("Failed rearranging documents", e);
return null; return null;

View file

@ -16,7 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class RotationController { public class RotationController {
@ -46,7 +46,7 @@ public class RotationController {
page.setRotation(page.getRotation() + angle); page.setRotation(page.getRotation() + angle);
} }
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
} }

View file

@ -0,0 +1,87 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
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.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
public class ScalePagesController {
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file.")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The scale of pages in the output PDF. Acceptable values are A4.", required = true, schema = @Schema(type = "String", allowableValues = { "A4" })) @RequestParam("pageSize") String targetPageSize,
@Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "float")) @RequestParam("scaleFactor") float scaleFactor)
throws IOException {
if (!targetPageSize.equals("A4")) {
throw new IllegalArgumentException("pageSize must be A4");
}
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
PageSize pageSize = new PageSize(PageSize.A4); // TODO: This (and all other PageSize.A4) need to be dynamically changed in response to targetPageSize
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(pageSize);
PdfCanvas pdfCanvas = new PdfCanvas(page);
// Get the page and calculate scaling factors
Rectangle rect = pdfDoc.getPage(i).getPageSize();
float scaleWidth = PageSize.A4.getWidth() / rect.getWidth();
float scaleHeight = PageSize.A4.getHeight() / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
System.out.println("Scale: " + scale);
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
float x = (PageSize.A4.getWidth() - rect.getWidth() * scale) / 2; // Center Page
float y = (PageSize.A4.getHeight() - rect.getHeight() * scale) / 2;
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
pdfCanvas.addXObject(formXObject, 0, 0);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
}
}

View file

@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class ConvertImgPDFController { public class ConvertImgPDFController {
@ -98,7 +99,7 @@ public class ConvertImgPDFController {
boolean autoRotate) throws IOException { boolean autoRotate) throws IOException {
// Convert the file to PDF and get the resulting bytes // Convert the file to PDF and get the resulting bytes
byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate, colorType); byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate, colorType);
return PdfUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_coverted.pdf"); return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_coverted.pdf");
} }
private String getMediaType(String imageFormat) { private String getMediaType(String imageFormat) {

View file

@ -17,8 +17,8 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class ConvertOfficeController { public class ConvertOfficeController {
@ -72,7 +72,7 @@ public class ConvertOfficeController {
// LibreOfficeListener.getInstance().start(); // LibreOfficeListener.getInstance().start();
byte[] pdfByteArray = convertToPdf(inputFile); byte[] pdfByteArray = convertToPdf(inputFile);
return PdfUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf"); return WebResponseUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf");
} }
} }

View file

@ -14,8 +14,8 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class ConvertPDFToPDFA { public class ConvertPDFToPDFA {
@ -58,7 +58,7 @@ public class ConvertPDFToPDFA {
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View file

@ -28,9 +28,9 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.ImageFinder; import stirling.software.SPDF.pdf.ImageFinder;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class BlankPageController { public class BlankPageController {
@ -109,7 +109,7 @@ public class BlankPageController {
} }
} }
return PdfUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf");
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);

View file

@ -31,8 +31,9 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class CompressController { public class CompressController {
@ -55,7 +56,7 @@ public class CompressController {
Long expectedOutputSize = 0L; Long expectedOutputSize = 0L;
boolean autoMode = false; boolean autoMode = false;
if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) { if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) {
expectedOutputSize = PdfUtils.convertSizeToBytes(expectedOutputSizeString); expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString);
autoMode = true; autoMode = true;
} }
@ -224,7 +225,7 @@ public class CompressController {
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View file

@ -31,8 +31,8 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class ExtractImageScansController { public class ExtractImageScansController {
@ -147,11 +147,11 @@ public class ExtractImageScansController {
// Clean up the temporary zip file // Clean up the temporary zip file
Files.delete(tempZipFile); Files.delete(tempZipFile);
return PdfUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else { } else {
// Return the processed image as a response // Return the processed image as a response
byte[] imageBytes = processedImageBytes.get(0); byte[] imageBytes = processedImageBytes.get(0);
return PdfUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG); return WebResponseUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG);
} }
} }

View file

@ -29,7 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class ExtractImagesController { public class ExtractImagesController {
@ -106,7 +106,7 @@ public class ExtractImagesController {
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
return PdfUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View file

@ -19,7 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class MetadataController { public class MetadataController {
@ -159,7 +159,7 @@ public class MetadataController {
info.setTrapped(trapped); info.setTrapped(trapped);
document.setDocumentInformation(info); document.setDocumentInformation(info);
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf");
} }
} }

View file

@ -27,8 +27,8 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class OCRController { public class OCRController {
@ -189,11 +189,11 @@ public class OCRController {
Files.delete(sidecarTextPath); Files.delete(sidecarTextPath);
// Return the zip file containing both the PDF and the text file // Return the zip file containing both the PDF and the text file
return PdfUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else { } else {
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View file

@ -15,6 +15,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class OverlayImageController { public class OverlayImageController {
@ -47,7 +48,7 @@ public class OverlayImageController {
byte[] imageBytes = imageFile.getBytes(); byte[] imageBytes = imageFile.getBytes();
byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage); byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage);
return PdfUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); return WebResponseUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to add image to PDF", e); logger.error("Failed to add image to PDF", e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

View file

@ -16,8 +16,8 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class RepairController { public class RepairController {
@ -60,7 +60,7 @@ public class RepairController {
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf";
return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View file

@ -2,6 +2,8 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.WebResponseUtils;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -51,7 +53,6 @@ import com.itextpdf.signatures.SignatureUtil;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils;
@RestController @RestController
public class CertSignController { public class CertSignController {
@ -239,7 +240,7 @@ public class CertSignController {
System.out.println("Signed PDF size: " + signedPdf.size()); System.out.println("Signed PDF size: " + signedPdf.size());
System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray())); System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray()));
return PdfUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf"); return WebResponseUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf");
} }
public boolean isPdfSigned(byte[] pdfData) throws IOException { public boolean isPdfSigned(byte[] pdfData) throws IOException {

View file

@ -17,7 +17,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class PasswordController { public class PasswordController {
@ -38,7 +38,7 @@ public class PasswordController {
String password) throws IOException { String password) throws IOException {
PDDocument document = PDDocument.load(fileInput.getBytes(), password); PDDocument document = PDDocument.load(fileInput.getBytes(), password);
document.setAllSecurityToBeRemoved(true); document.setAllSecurityToBeRemoved(true);
return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf");
} }
@PostMapping(consumes = "multipart/form-data", value = "/add-password") @PostMapping(consumes = "multipart/form-data", value = "/add-password")
@ -105,7 +105,7 @@ public class PasswordController {
document.protect(spp); document.protect(spp);
return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
} }

View file

@ -19,7 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
public class WatermarkController { public class WatermarkController {
@ -91,7 +91,7 @@ public class WatermarkController {
// Close the content stream // Close the content stream
contentStream.close(); contentStream.close();
} }
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
} }
} }

View file

@ -115,6 +115,11 @@ public class OtherWebController {
return "other/multi-page-layout"; return "other/multi-page-layout";
} }
@GetMapping("/scale-pages")
@Hidden
public String scalePagesFrom(Model model) {
model.addAttribute("currentPage", "scale-pages");
return "other/scale-pages";
}
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.pdf;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.io.IOException; import java.io.IOException;

View file

@ -0,0 +1,30 @@
package stirling.software.SPDF.utils;
public class GeneralUtils {
public static Long convertSizeToBytes(String sizeStr) {
if (sizeStr == null) {
return null;
}
sizeStr = sizeStr.trim().toUpperCase();
try {
if (sizeStr.endsWith("KB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
} else if (sizeStr.endsWith("MB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024);
} else if (sizeStr.endsWith("GB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024);
} else if (sizeStr.endsWith("B")) {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} else {
// Input string does not have a valid format, handle this case
}
} catch (NumberFormatException e) {
// The numeric part of the input string cannot be parsed, handle this case
}
return null;
}
}

View file

@ -0,0 +1,25 @@
package stirling.software.SPDF.utils;
import java.awt.image.BufferedImage;
public class ImageProcessingUtils {
static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) {
BufferedImage convertedImage;
switch (colorType) {
case "greyscale":
convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
case "blackwhite":
convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
default: // full color
convertedImage = sourceImage;
break;
}
return convertedImage;
}
}

View file

@ -0,0 +1,5 @@
package stirling.software.SPDF.utils;
public class PDFManipulationUtils {
}

View file

@ -92,6 +92,6 @@ public class PDFToFile {
if (tempOutputDir != null) if (tempOutputDir != null)
FileUtils.deleteDirectory(tempOutputDir.toFile()); FileUtils.deleteDirectory(tempOutputDir.toFile());
} }
return PdfUtils.bytesToWebResponse(fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View file

@ -8,8 +8,6 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
@ -37,39 +35,12 @@ import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
public class PdfUtils { public class PdfUtils {
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class);
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException {
return PdfUtils.bytesToWebResponse(baos.toByteArray(), docName);
}
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
return PdfUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
}
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException {
// Return the PDF as a response
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
headers.setContentLength(bytes.length);
String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
headers.setContentDispositionFormData("attachment", encodedDocName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName) throws IOException {
return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF);
}
public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, String filename) throws IOException, Exception { public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, String filename) throws IOException, Exception {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
@ -134,7 +105,7 @@ public class PdfUtils {
int numPages = reader.getNumImages(true); int numPages = reader.getNumImages(true);
for (int i = 0; i < numPages; i++) { for (int i = 0; i < numPages; i++) {
BufferedImage pageImage = reader.read(i); BufferedImage pageImage = reader.read(i);
BufferedImage convertedImage = convertColorType(pageImage, colorType); BufferedImage convertedImage = ImageProcessingUtils.convertColorType(pageImage, colorType);
PDImageXObject pdImage = LosslessFactory.createFromImage(doc, convertedImage); PDImageXObject pdImage = LosslessFactory.createFromImage(doc, convertedImage);
addImageToDocument(doc, pdImage, stretchToFit, autoRotate); addImageToDocument(doc, pdImage, stretchToFit, autoRotate);
} }
@ -147,7 +118,7 @@ public class PdfUtils {
fos.write(buffer, 0, len); fos.write(buffer, 0, len);
} }
BufferedImage image = ImageIO.read(imageFile); BufferedImage image = ImageIO.read(imageFile);
BufferedImage convertedImage = convertColorType(image, colorType); BufferedImage convertedImage = ImageProcessingUtils.convertColorType(image, colorType);
PDImageXObject pdImage; PDImageXObject pdImage;
if (contentType != null && (contentType.equals("image/jpeg"))) { if (contentType != null && (contentType.equals("image/jpeg"))) {
pdImage = JPEGFactory.createFromImage(doc, convertedImage); pdImage = JPEGFactory.createFromImage(doc, convertedImage);
@ -170,24 +141,6 @@ public class PdfUtils {
} }
} }
private static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) {
BufferedImage convertedImage;
switch (colorType) {
case "greyscale":
convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
case "blackwhite":
convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
default: // full color
convertedImage = sourceImage;
break;
}
return convertedImage;
}
private static void addImageToDocument(PDDocument doc, PDImageXObject image, boolean stretchToFit, boolean autoRotate) throws IOException { private static void addImageToDocument(PDDocument doc, PDImageXObject image, boolean stretchToFit, boolean autoRotate) throws IOException {
boolean imageIsLandscape = image.getWidth() > image.getHeight(); boolean imageIsLandscape = image.getWidth() > image.getHeight();
PDRectangle pageSize = PDRectangle.A4; PDRectangle pageSize = PDRectangle.A4;
@ -224,33 +177,6 @@ public class PdfUtils {
} }
} }
public static X509Certificate[] loadCertificateChainFromKeystore(InputStream keystoreInputStream, String keystorePassword) throws Exception {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(keystoreInputStream, keystorePassword.toCharArray());
String alias = keystore.aliases().nextElement();
Certificate[] certChain = keystore.getCertificateChain(alias);
X509Certificate[] x509CertChain = new X509Certificate[certChain.length];
for (int i = 0; i < certChain.length; i++) {
x509CertChain[i] = (X509Certificate) certChain[i];
}
return x509CertChain;
}
public static KeyPair loadKeyPairFromKeystore(InputStream keystoreInputStream, String keystorePassword) throws Exception {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(keystoreInputStream, keystorePassword.toCharArray());
String alias = keystore.aliases().nextElement();
PrivateKey privateKey = (PrivateKey) keystore.getKey(alias, keystorePassword.toCharArray());
Certificate cert = keystore.getCertificate(alias);
PublicKey publicKey = cert.getPublicKey();
return new KeyPair(publicKey, privateKey);
}
public static byte[] overlayImage(byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) throws IOException { public static byte[] overlayImage(byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) throws IOException {
PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes)); PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes));
@ -282,41 +208,7 @@ public class PdfUtils {
return baos.toByteArray(); return baos.toByteArray();
} }
public static ResponseEntity<byte[]> pdfDocToWebResponse(PDDocument document, String docName) throws IOException {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
// Close the document
document.close();
return PdfUtils.boasToWebResponse(baos, docName);
}
public static Long convertSizeToBytes(String sizeStr) {
if (sizeStr == null) {
return null;
}
sizeStr = sizeStr.trim().toUpperCase();
try {
if (sizeStr.endsWith("KB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
} else if (sizeStr.endsWith("MB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024);
} else if (sizeStr.endsWith("GB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024);
} else if (sizeStr.endsWith("B")) {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} else {
// Input string does not have a valid format, handle this case
}
} catch (NumberFormatException e) {
// The numeric part of the input string cannot be parsed, handle this case
}
return null;
}
} }

View file

@ -0,0 +1,50 @@
package stirling.software.SPDF.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
public class WebResponseUtils {
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
}
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
}
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException {
// Return the PDF as a response
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
headers.setContentLength(bytes.length);
String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
headers.setContentDispositionFormData("attachment", encodedDocName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName) throws IOException {
return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF);
}
public static ResponseEntity<byte[]> pdfDocToWebResponse(PDDocument document, String docName) throws IOException {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
// Close the document
document.close();
return boasToWebResponse(baos, docName);
}
}

View file

@ -131,7 +131,10 @@ home.certSign.desc=Signs a PDF with a Certificate/Key (PEM/P12)
home.pageLayout.title=Multi-Page Layout home.pageLayout.title=Multi-Page Layout
home.pageLayout.desc=Merge multiple pages of a PDF document into a single page home.pageLayout.desc=Merge multiple pages of a PDF document into a single page
home.scalePages.title=Adjust page-scale
home.scalePages.desc=Change the size of the pages of a PDF document
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
downloadPdf=Download PDF downloadPdf=Download PDF
text=Text text=Text
@ -144,6 +147,12 @@ pageLayout.header=Multi Page Layout
pageLayout.pagesPerSheet=Pages per sheet: pageLayout.pagesPerSheet=Pages per sheet:
pageLayout.submit=Submit pageLayout.submit=Submit
scalePages.title=Adjust page-scale
scalePages.header=Adjust page-scale
scalePages.pageSize=Size of a page of the document.
scalePages.scaleFactor=Zoom level (crop) of a page.
scalePages.submit=Submit
certSign.title=Certificate Signing certSign.title=Certificate Signing
certSign.header=Sign a PDF with your certificate (Work in progress) certSign.header=Sign a PDF with your certificate (Work in progress)
certSign.selectPDF=Select a PDF File for Signing: certSign.selectPDF=Select a PDF File for Signing:

View file

@ -131,6 +131,11 @@ home.certSign.desc=Podpisz dokument PDF za pomocą certyfikatu/klucza prywatnego
home.pageLayout.title=Układ wielu stron home.pageLayout.title=Układ wielu stron
home.pageLayout.desc=Scal wiele stron dokumentu PDF w jedną stronę home.pageLayout.desc=Scal wiele stron dokumentu PDF w jedną stronę
home.scalePages.title=Dopasuj rozmiar stron
home.scalePages.desc=Dopasuj rozmiar stron wybranego dokumentu PDF
error.pdfPassword=Dokument PDF jest zabezpieczony hasłem, musisz podać prawidłowe hasło.
downloadPdf=Pobierz PDF downloadPdf=Pobierz PDF
text=Tekst text=Tekst
font=Czcionka font=Czcionka
@ -142,6 +147,12 @@ pageLayout.header=Układ wielu stron
pageLayout.pagesPerSheet=Stron na jednym arkuszu: pageLayout.pagesPerSheet=Stron na jednym arkuszu:
pageLayout.submit=Wykonaj pageLayout.submit=Wykonaj
scalePages.title=Dopasuj rozmiar stron
scalePages.header=Dopasuj rozmiar stron
scalePages.pageSize=Rozmiar stron dokumentu:
scalePages.scaleFactor=Poziom powiększenia (przycięcia) stron:
scalePages.submit=Wykonaj
certSign.title=Podpisywanie certyfikatem certSign.title=Podpisywanie certyfikatem
certSign.header=Podpisz dokument PDF certyfikatem prywatnym (moduł w budowie) certSign.header=Podpisz dokument PDF certyfikatem prywatnym (moduł w budowie)
certSign.selectPDF=Wybierz dokument PDF do podpisania: certSign.selectPDF=Wybierz dokument PDF do podpisania:

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrows-fullscreen" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View file

@ -103,7 +103,7 @@ margin-top: 0;
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
<div class="container"> <div class="container">
<div id="support-section"> <div id="support-section">

View file

@ -220,267 +220,212 @@ document.addEventListener("DOMContentLoaded", function () {
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: 'true', notRequired=${notRequired} ?: false"> <th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: 'true', notRequired=${notRequired} ?: false">
<script> <script>
$(document).ready(function() { function showErrorBanner(message, stackTrace) {
$('form').submit(async function(event) { const errorContainer = document.getElementById("errorContainer");
const boredWaiting = localStorage.getItem('boredWaiting'); errorContainer.style.display = "block"; // Display the banner
if (boredWaiting === 'enabled') { document.querySelector("#errorContainer .alert-heading").textContent = "Error";
$('#show-game-btn').show(); document.querySelector("#errorContainer p").textContent = message;
} document.querySelector("#traceContent").textContent = stackTrace;
var processing = "Processing..." }
var submitButtonText = $('#submitBtn').text()
$(document).ready(function () {
$('#submitBtn').text('Processing...'); $('form').submit(async function (event) {
console.log("start download code") event.preventDefault();
var files = $('#fileInput-input')[0].files;
var url = this.action; const url = this.action;
console.log(url) const files = $('#fileInput-input')[0].files;
event.preventDefault(); // Prevent the default form handling behavior const formData = new FormData(this);
const override = $('#override').val() || '';
/* Check if ${multiple} is false */ $('#submitBtn').text('Processing...');
var multiple = [[${multiple}]] || false;
var override = $('#override').val() || ''; try {
console.log("override=" + override) if (override === 'multi' || files.length > 1 && override !== 'single') {
await submitMultiPdfForm(url, files);
if([[${remoteCall}]] === true) { } else {
if (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) { const downloadDetails = await handleSingleDownload(url, formData);
console.log("multi parallel download")
await submitMultiPdfForm(event,url);
} else {
console.log("start single download")
// Get the selected download option from localStorage
const downloadOption = localStorage.getItem('downloadOption');
var formData = new FormData($('form')[0]);
// Send the request to the server using the fetch() API
const response = await fetch(url, {
method: 'POST',
body: formData
});
try {
if (!response) {
throw new Error('Received null response for file ' + i);
}
console.log("load single download")
// Extract the filename from the Content-Disposition header, if present
let filename = null;
const contentDispositionHeader = response.headers.get('Content-Disposition');
console.log(contentDispositionHeader)
if (contentDispositionHeader && contentDispositionHeader.indexOf('attachment') !== -1) {
filename = decodeURIComponent(contentDispositionHeader.split('filename=')[1].replace(/"/g, ''));
} else {
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
filename = 'download';
}
console.log("filename=" + filename)
const contentType = response.headers.get('Content-Type');
console.log("contentType=" + contentType)
// Check if the response is a PDF or an image
if (contentType.includes('pdf') || contentType.includes('image')) {
const blob = await response.blob();
console.log("pdf/image")
// Perform the appropriate action based on the download option
if (downloadOption === 'sameWindow') {
console.log("same window")
// Open the file in the same window
window.location.href = URL.createObjectURL(blob);
} else if (downloadOption === 'newWindow') {
console.log("new window")
// Open the file in a new window
window.open(URL.createObjectURL(blob), '_blank');
} else {
console.log("else save")
// Download the file
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
} else if (contentType.includes('json')) {
// Handle the JSON response
const json = await response.json();
// Format the error message
const errorMessage = JSON.stringify(json, null, 2);
// Display the error message in an alert
alert(`An error occurred: ${errorMessage}`);
} else {
const blob = await response.blob()
console.log("else save 2 zip")
// For ZIP files or other file types, just download the file
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
} catch(error) {
console.log("error listener")
// Extract the error message and stack trace from the response
const errorMessage = error.message;
const stackTrace = error.stack;
// Create an error message to display to the user
const message = `${errorMessage}\n\n${stackTrace}`;
$('#submitBtn').text(submitButtonText);
// Display the error message to the user
alert(message);
};
}
} else {
//Offline do nothing and let other scripts handle everything
}
$('#submitBtn').text(submitButtonText);
});
});
async function submitMultiPdfForm(event, url) {
// Get the selected PDF files
let files = $('#fileInput-input')[0].files;
// Get the existing form data // Determine the download option from localStorage
let formData = new FormData($('form')[0]); const downloadOption = localStorage.getItem('downloadOption');
formData.delete('fileInput');
// Show the progress bar // Handle the download action according to the selected option
$('#progressBarContainer').show(); handleDownloadAction(downloadOption, downloadDetails.blob, downloadDetails.filename);
}
// Initialize the progress bar $('#submitBtn').text('Submit');
let progressBar = $('#progressBar'); } catch (error) {
progressBar.css('width', '0%'); handleDownloadError(error);
progressBar.attr('aria-valuenow', 0); $('#submitBtn').text('Submit');
progressBar.attr('aria-valuemax', files.length); }
});
});
// Check the flag in localStorage, default to 4 function handleDownloadAction(downloadOption, blob, filename) {
const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4; const url = URL.createObjectURL(blob);
const zipFiles = files.length > zipThreshold;
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;
}
}
// Initialize JSZip instance if needed async function handleSingleDownload(url, formData) {
let jszip = null; const response = await fetch(url, {
if (zipFiles) { method: 'POST',
jszip = new JSZip(); body: formData
} });
// Submit each PDF file in parallel if (!response.ok) {
let promises = []; throw new Error(`HTTP error! status: ${response.status}`);
for (let i = 0; i < files.length; i++) { } else {
let promise = new Promise(async function(resolve, reject) { const blob = await response.blob();
let fileFormData = new FormData(); const filename = getFilenameFromContentDisposition(response.headers.get('Content-Disposition'));
fileFormData.append('fileInput', files[i]); return { blob, filename };
for (let pair of formData.entries()) { }
fileFormData.append(pair[0], pair[1]); }
} function getFilenameFromContentDisposition(contentDisposition) {
console.log(fileFormData); let filename;
try { if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) {
let response = await fetch(url, { filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, ''));
method: 'POST', } else {
body: fileFormData // If the Content-Disposition header is not present or does not contain the filename, use a default filename
}); filename = 'download';
}
if (!response) { return filename;
throw new Error('Received null response for file ' + i); }
}
if (!response.ok) {
throw new Error(`Error submitting request for file ${i}: ${response.status} ${response.statusText}`);
}
let contentDisposition = response.headers.get('content-disposition');
let fileName = "file.pdf"
if (!contentDisposition) {
//throw new Error('Content-Disposition header not found for file ' + i);
} else {
fileName = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, ''));
}
console.log('Received response for file ' + i + ': ' + response);
let blob = await response.blob(); async function handlePdfOrImageResponse(response, filename) {
if (zipFiles) { const downloadOption = localStorage.getItem('downloadOption');
// Add the file to the ZIP archive const blob = await response.blob();
jszip.file(fileName, blob); const url = URL.createObjectURL(blob);
resolve(); if (downloadOption === 'sameWindow') {
} else { window.location.href = url;
// Download the file directly } else if (downloadOption === 'newWindow') {
let url = window.URL.createObjectURL(blob); window.open(url, '_blank');
let a = document.createElement('a'); } else {
a.href = url; downloadFile(url, filename);
a.download = fileName; }
document.body.appendChild(a); }
a.click();
a.remove();
resolve();
}
} catch (error) {
console.error('Error submitting request for file ' + i + ': ' + error);
// Set default values or fallbacks for error properties async function handleJsonResponse(response) {
let status = error && error.status || 500; const json = await response.json();
let statusText = error && error.statusText || 'Internal Server Error'; const errorMessage = JSON.stringify(json, null, 2);
let message = error && error.message || 'An error occurred while processing your request.'; if(errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided')){
alert('[[#{error.pdfPassword}]]');
} else {
showErrorBanner(json.error + ':' + json.message, json.trace);
}
}
// Reject the Promise to signal that the request has failed async function handleOtherResponse(response, filename) {
reject(); const blob = await response.blob();
// Redirect to error page with Spring Boot error parameters const url = URL.createObjectURL(blob);
let url = '/error?status=' + status + '&error=' + encodeURIComponent(statusText) + '&message=' + encodeURIComponent(message); downloadFile(url, filename);
window.location.href = url; }
} function handleDownloadError(error) {
}); const errorMessage = error.message;
showErrorBanner(errorMessage);
}
// Update the progress bar as each request finishes let urls = []; // An array to hold all the URLs
promise.then(function() {
updateProgressBar(progressBar, files);
});
promises.push(promise); function downloadFile(blob, filename) {
} const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
urls.push(url); // Store the URL so it doesn't get garbage collected too soon
}
// Wait for all requests to finish
try { async function submitMultiPdfForm(url, files) {
await Promise.all(promises); const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
} catch (error) { const zipFiles = files.length > zipThreshold;
console.error('Error while uploading files: ' + error); let jszip = null;
} let progressBar = $('#progressBar');
progressBar.css('width', '0%');
progressBar.attr('aria-valuenow', 0);
progressBar.attr('aria-valuemax', Array.from(files).length);
if (zipFiles) {
jszip = new JSZip();
}
// Update the progress bar // Get existing form data
progressBar.css('width', '100%'); let formData = new FormData($('form')[0]);
progressBar.attr('aria-valuenow', files.length); formData.delete('fileInput');
// After all requests are finished, download the ZIP file if needed const CONCURRENCY_LIMIT = 8;
if (zipFiles) { const chunks = [];
try { for (let i = 0; i < Array.from(files).length; i += CONCURRENCY_LIMIT) {
let content = await jszip.generateAsync({ type: "blob" }); chunks.push(Array.from(files).slice(i, i + CONCURRENCY_LIMIT));
let url = window.URL.createObjectURL(content); }
let a = document.createElement('a');
a.href = url; for (const chunk of chunks) {
a.download = "files.zip"; const promises = chunk.map(async file => {
document.body.appendChild(a); let fileFormData = new FormData();
a.click(); fileFormData.append('fileInput', file);
a.remove();
} catch (error) { // Add other form data
console.error('Error generating ZIP file: ' + error); for (let pair of formData.entries()) {
} fileFormData.append(pair[0], pair[1]);
} }
}
try {
const downloadDetails = await handleSingleDownload(url, fileFormData);
if (zipFiles) {
jszip.file(downloadDetails.filename, downloadDetails.blob);
} else {
downloadFile(downloadDetails.blob, downloadDetails.filename);
}
updateProgressBar(progressBar, Array.from(files).length);
} catch (error) {
handleDownloadError(error);
}
});
await Promise.all(promises);
}
if (zipFiles) {
try {
const content = await jszip.generateAsync({ type: "blob" });
downloadFile(content, "files.zip");
} catch (error) {
console.error('Error generating ZIP file: ' + error);
}
}
}
function updateProgressBar(progressBar, files) { function updateProgressBar(progressBar, files) {
let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length); let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length);
progressBar.css('width', progress + '%'); progressBar.css('width', progress + '%');
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1); progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1);
} }
window.addEventListener('unload', () => {
for (const url of urls) {
URL.revokeObjectURL(url);
}
});
</script> </script>
<div class="custom-file-chooser"> <div class="custom-file-chooser">

View file

@ -0,0 +1,225 @@
<th:block th:fragment="errorBannerPerPage">
<style scoped>
#github-button,
#discord-button {
display: inline-block;
padding: 1rem 2rem;
background-color: #008CBA;
color: #fff;
text-align: center;
text-decoration: none;
border-radius: 3rem;
transition: all 0.3s ease-in-out;
}
#github-button:hover,
#discord-button:hover {
background-color: #005b7f;
}
</style>
<div id="errorContainer" class="alert alert-danger alert-dismissible fade show" role="alert" style="display: none;">
<h4 class="alert-heading">Error</h4>
<p></p>
<button type="button" class="btn btn-danger" onclick="toggletrace()">Show Stack Trace</button>
<button type="button" class="btn btn-secondary" onclick="copytrace()">Copy Stack Trace</button>
<button type="button" class="btn btn-info" onclick="showHelp()">Help</button>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span aria-hidden="true">&times;</span>
</button>
<!-- Stack trace section -->
<div id="trace" style="max-height: 0; overflow: hidden;">
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 3px; padding: 10px; margin-top: 5px;">
<pre id="traceContent"></pre>
</div>
</div>
</div>
<!-- Help Modal -->
<style scoped>
#errorContainer {
margin: 20px; /* adjust this value as needed */
}
#helpModalDialog {
width: 90%;
max-width: 800px;
}
#helpModal h1 {
text-align: center;
margin-top: 10%;
}
#helpModal p {
text-align: center;
margin-top: 2em;
}
#helpModal .button:hover {
background-color: #005b7f;
}
#helpModal .features-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr));
gap: 25px 30px;
}
#helpModal .feature-card {
border: 1px solid rgba(0, 0, 0, .125);
border-radius: 0.25rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: flex-start;
}
#helpModal .feature-card .card-text {
flex: 1;
}
#support-section {
background-color: #f9f9f9;
padding: 4rem;
margin-top: 1rem;
text-align: center;
}
#support-section h1 {
margin-top: 0;
}
#support-section p {
margin-top: 0;
}
#button-group {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
#github-button, #discord-button {
display: inline-block;
padding: 1rem 2rem;
margin: 1rem;
background-color: #008CBA;
color: #fff;
font-size: 1.2rem;
text-align: center;
text-decoration: none;
border-radius: 3rem;
transition: all 0.3s ease-in-out;
}
#github-button:hover, #discord-button:hover, #home-button:hover {
background-color: #005b7f;
}
#home-button {
display: block;
width: 200px;
height: 50px;
margin: 2em auto;
background-color: #008CBA;
color: white;
text-align: center;
line-height: 50px;
text-decoration: none;
font-weight: bold;
border-radius: 25px;
transition: all 0.3s ease-in-out;
}
</style>
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document" id="helpModalDialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="helpModalLabel">Help</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container">
<div id="support-section">
<h1 class="display-2">Oops!</h1>
<p class="lead">Sorry for the issue!.</p>
<br>
<h2>Need help / Found an issue?</h2>
<p>If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:</p>
<div id="button-group">
<a href="https://github.com/Frooodle/Stirling-PDF/issues" id="github-button" target="_blank">GitHub - Submit a ticket</a>
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Discord - Submit Support post</a>
</div>
<a href="/" id="home-button">Go to Homepage</a>
<a data-dismiss="modal" id="home-button">Close</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var traceVisible = false;
function toggletrace() {
var traceDiv = document.getElementById("trace");
if (!traceVisible) {
traceDiv.style.maxHeight = "500px";
traceVisible = true;
} else {
traceDiv.style.maxHeight = "0px";
traceVisible = false;
}
adjustContainerHeight();
}
function copytrace() {
var flip = false
if(!traceVisible) {
toggletrace()
flip = true
}
var traceContent = document.getElementById("traceContent");
var range = document.createRange();
range.selectNode(traceContent);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
if(flip){
toggletrace()
}
}
function dismissError() {
var errorContainer = document.getElementById("errorContainer");
errorContainer.style.display = "none";
errorContainer.style.height ="0";
}
function adjustContainerHeight() {
var errorContainer = document.getElementById("errorContainer");
var traceDiv = document.getElementById("trace");
if (traceVisible) {
errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px";
} else {
errorContainer.style.height = "auto";
}
}
function showHelp() {
$('#helpModal').modal('show');
}
</script>
</th:block>

View file

@ -157,6 +157,7 @@ function compareVersions(version1, version2) {
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc')}"></div>
</div> </div>
</li> </li>
@ -349,8 +350,8 @@ function compareVersions(version1, version2) {
</script> </script>
</nav> </nav>
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
<div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div>
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel" aria-hidden="true"> <div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content dark-card"> <div class="modal-content dark-card">

View file

@ -137,7 +137,8 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
<div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div> <div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', svgPath='images/scale-pages.svg')}"></div>
<script> <script>

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{scalePages.title})}"></th:block>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{scalePages.header}"></h2>
<form id="scalePagesFrom" th:action="@{scale-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group">
<label for="pageSize" th:text="#{scalePages.pageSize}"></label>
<select id="pageSize" name="pageSize" required>
<option value="A4">A4</option>
</select>
</div>
<div class="form-group">
<label for="scaleFactor" th:text="#{scalePages.scaleFactor}"></label>
<input type="number" id="scaleFactor" name="scaleFactor" step="any" min="0" value="1">
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{scalePages.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>