diff --git a/DockerfileBase b/DockerfileBase index 876b5a10..d43f0e65 100644 --- a/DockerfileBase +++ b/DockerfileBase @@ -1,27 +1,3 @@ -# Build jbig2enc in a separate stage -FROM debian:bullseye-slim as jbig2enc_builder - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - git \ - automake \ - autoconf \ - libtool \ - libleptonica-dev \ - pkg-config \ - ca-certificates \ - zlib1g-dev \ - make \ - g++ - -RUN git clone https://github.com/agl/jbig2enc && \ - cd jbig2enc && \ - ./autogen.sh && \ - ./configure && \ - make && \ - make install - - # Main stage FROM openjdk:17-jdk-slim AS base RUN apt-get update && \ @@ -58,5 +34,4 @@ RUN apt-get update && \ # Final stage: Copy necessary files from the previous stage FROM base -COPY --from=python-packages /usr/local /usr/local -COPY --from=jbig2enc_builder /usr/local/bin/jbig2 /usr/local/bin/jbig2 \ No newline at end of file +COPY --from=python-packages /usr/local /usr/local \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java index 009cd6e9..2671e1b5 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java @@ -5,8 +5,8 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.IOException; +import org.apache.commons.io.FileUtils; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -45,55 +45,98 @@ public class CompressController { @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") @Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters.") public ResponseEntity optimizePdf( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile, - @RequestParam("optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = { - "0", "1", "2", "3" }), example = "1") int optimizeLevel, - @RequestParam("expectedOutputSize") @Parameter(description = "The expected output size in bytes.", required = false) Long expectedOutputSize) - throws IOException, InterruptedException { + @RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile, + @RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = { + "1", "2", "3", "4", "5" })) Integer optimizeLevel, + @RequestParam(value = "expectedOutputSize", required = false) @Parameter(description = "The expected output size, e.g. '100MB', '25KB', etc.", required = false) String expectedOutputSizeString) + throws Exception { + + if(expectedOutputSizeString == null && optimizeLevel == null) { + throw new Exception("Both expected output size and optimize level are not specified"); + } + + Long expectedOutputSize = 0L; + if (expectedOutputSizeString != null) { + expectedOutputSize = PdfUtils.convertSizeToBytes(expectedOutputSizeString); + } // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); inputFile.transferTo(tempInputFile.toFile()); + long inputFileSize = Files.size(tempInputFile); + // Prepare the output file path Path tempOutputFile = Files.createTempFile("output_", ".pdf"); - // Prepare the Ghostscript command - List command = new ArrayList<>(); - command.add("gs"); - command.add("-sDEVICE=pdfwrite"); - command.add("-dCompatibilityLevel=1.4"); - - switch (optimizeLevel) { - case 0: - command.add("-dPDFSETTINGS=/default"); - break; - case 1: - command.add("-dPDFSETTINGS=/ebook"); - break; - case 2: - command.add("-dPDFSETTINGS=/printer"); - break; - case 3: - command.add("-dPDFSETTINGS=/prepress"); - break; - default: - command.add("-dPDFSETTINGS=/default"); + // Determine initial optimization level based on expected size reduction, only if optimizeLevel is not provided + if(optimizeLevel == null) { + double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; + if (sizeReductionRatio > 0.7) { + optimizeLevel = 1; + } else if (sizeReductionRatio > 0.5) { + optimizeLevel = 2; + } else if (sizeReductionRatio > 0.35) { + optimizeLevel = 3; + } else { + optimizeLevel = 4; + } } - command.add("-dNOPAUSE"); - command.add("-dQUIET"); - command.add("-dBATCH"); - command.add("-sOutputFile=" + tempOutputFile.toString()); - command.add(tempInputFile.toString()); + boolean sizeMet = expectedOutputSize == 0L; + while (!sizeMet && optimizeLevel <= 5) { + // Prepare the Ghostscript command + List command = new ArrayList<>(); + command.add("gs"); + command.add("-sDEVICE=pdfwrite"); + command.add("-dCompatibilityLevel=1.4"); - int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + switch (optimizeLevel) { + case 1: + command.add("-dPDFSETTINGS=/prepress"); + break; + case 2: + command.add("-dPDFSETTINGS=/printer"); + break; + case 3: + command.add("-dPDFSETTINGS=/default"); + break; + case 4: + command.add("-dPDFSETTINGS=/ebook"); + break; + case 5: + command.add("-dPDFSETTINGS=/screen"); + break; + default: + command.add("-dPDFSETTINGS=/default"); + } + + command.add("-dNOPAUSE"); + command.add("-dQUIET"); + command.add("-dBATCH"); + command.add("-sOutputFile=" + tempOutputFile.toString()); + command.add(tempInputFile.toString()); + + int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + + // Check if file size is within expected size + long outputFileSize = Files.size(tempOutputFile); + if (outputFileSize <= expectedOutputSize) { + sizeMet = true; + } else { + // Increase optimization level for next iteration + optimizeLevel++; + System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel); + } + } + + if (expectedOutputSize != null) { long outputFileSize = Files.size(tempOutputFile); if (outputFileSize > expectedOutputSize) { try (PDDocument doc = PDDocument.load(new File(tempOutputFile.toString()))) { - + long previousFileSize = 0; double scaleFactor = 1.0; while (true) { for (PDPage page : doc.getPages()) { @@ -142,14 +185,21 @@ public class CompressController { // save the document to tempOutputFile again doc.save(tempOutputFile.toString()); + long currentSize = Files.size(tempOutputFile); // Check if the overall PDF size is still larger than expectedOutputSize - if (Files.size(tempOutputFile) > expectedOutputSize) { + if (currentSize > expectedOutputSize) { + // Log the current file size and scaleFactor + + System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize)); + System.out.println("Current scale factor: " + scaleFactor); + // The file is still too large, reduce scaleFactor and try again scaleFactor *= 0.9; // reduce scaleFactor by 10% // Avoid scaleFactor being too small, causing the image to shrink to 0 - if(scaleFactor < 0.1){ - throw new RuntimeException("Could not reach the desired size without excessively degrading image quality"); + if(scaleFactor < 0.2 || previousFileSize == currentSize){ + throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes"); } + previousFileSize = currentSize; } else { // The file is small enough, break the loop break; diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 2fd871b0..d3427f79 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -22,6 +22,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -287,4 +288,30 @@ public class PdfUtils { 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.parseLong(sizeStr.substring(0, sizeStr.length() - 2)) * 1024; + } else if (sizeStr.endsWith("MB")) { + return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024; + } else if (sizeStr.endsWith("GB")) { + return Long.parseLong(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; + } + } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 2b33ccf3..2604350f 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -224,12 +224,7 @@ compress.title=Compress compress.header=Compress PDF compress.credit=This service uses OCRmyPDF for PDF Compress/Optimisation. compress.selectText.1=Optimization level: -compress.selectText.2=0 (No optimization) -compress.selectText.3=1 (Default, lossless optimization) -compress.selectText.4=2 (Lossy optimization) -compress.selectText.5=3 (Lossy optimization, more aggressive) -compress.selectText.6=Enable fast web view (linearize PDF) -compress.selectText.7=Enable lossy JBIG2 encoding +compress.selectText.2=Expected PDF Size (e.g. 100MB, 25KB, 500B) compress.submit=Compress diff --git a/src/main/resources/static/css/dark-mode.css b/src/main/resources/static/css/dark-mode.css index 3cc945db..0e78e4ae 100644 --- a/src/main/resources/static/css/dark-mode.css +++ b/src/main/resources/static/css/dark-mode.css @@ -5,6 +5,11 @@ body { background-color: rgb(var(--body-background-color)) !important; color: rgb(var(--base-font-color)) !important; } +.card { + background-color: rgb(var(--body-background-color)) !important; + border: 1px solid #999; + color: rgb(var(--base-font-color)) !important; +} .dark-card { background-color: rgb(var(--body-background-color)) !important; diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 5689eaab..da2a4c70 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -71,6 +71,11 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg); .favorite-icon img { filter: brightness(0); } + +.jumbotron { + padding: 3rem 3rem; /* Reduce vertical padding */ +} + diff --git a/src/main/resources/templates/other/compress-pdf.html b/src/main/resources/templates/other/compress-pdf.html index a082e622..bd3931ae 100644 --- a/src/main/resources/templates/other/compress-pdf.html +++ b/src/main/resources/templates/other/compress-pdf.html @@ -17,24 +17,29 @@

-
- - +
+
+

Manual Mode - From 1 to 5

+ + +
-
- - + +
+
+

Auto mode - Auto adjusts quality to get PDF to exact size

+ + +
- -

-