mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-05-01 01:16:43 +02:00
as
This commit is contained in:
parent
87cd6dfb54
commit
9d80458250
@ -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
|
# Main stage
|
||||||
FROM openjdk:17-jdk-slim AS base
|
FROM openjdk:17-jdk-slim AS base
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
@ -58,5 +34,4 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
# Final stage: Copy necessary files from the previous stage
|
# Final stage: Copy necessary files from the previous stage
|
||||||
FROM base
|
FROM base
|
||||||
COPY --from=python-packages /usr/local /usr/local
|
COPY --from=python-packages /usr/local /usr/local
|
||||||
COPY --from=jbig2enc_builder /usr/local/bin/jbig2 /usr/local/bin/jbig2
|
|
@ -5,8 +5,8 @@ import java.awt.image.BufferedImage;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -45,55 +45,98 @@ public class CompressController {
|
|||||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
@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.")
|
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters.")
|
||||||
public ResponseEntity<byte[]> optimizePdf(
|
public ResponseEntity<byte[]> optimizePdf(
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
|
@RequestPart(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 = {
|
@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 = {
|
||||||
"0", "1", "2", "3" }), example = "1") int optimizeLevel,
|
"1", "2", "3", "4", "5" })) Integer optimizeLevel,
|
||||||
@RequestParam("expectedOutputSize") @Parameter(description = "The expected output size in bytes.", required = false) Long expectedOutputSize)
|
@RequestParam(value = "expectedOutputSize", required = false) @Parameter(description = "The expected output size, e.g. '100MB', '25KB', etc.", required = false) String expectedOutputSizeString)
|
||||||
throws IOException, InterruptedException {
|
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
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
|
|
||||||
|
long inputFileSize = Files.size(tempInputFile);
|
||||||
|
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Prepare the Ghostscript command
|
// Determine initial optimization level based on expected size reduction, only if optimizeLevel is not provided
|
||||||
List<String> command = new ArrayList<>();
|
if(optimizeLevel == null) {
|
||||||
command.add("gs");
|
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
||||||
command.add("-sDEVICE=pdfwrite");
|
if (sizeReductionRatio > 0.7) {
|
||||||
command.add("-dCompatibilityLevel=1.4");
|
optimizeLevel = 1;
|
||||||
|
} else if (sizeReductionRatio > 0.5) {
|
||||||
switch (optimizeLevel) {
|
optimizeLevel = 2;
|
||||||
case 0:
|
} else if (sizeReductionRatio > 0.35) {
|
||||||
command.add("-dPDFSETTINGS=/default");
|
optimizeLevel = 3;
|
||||||
break;
|
} else {
|
||||||
case 1:
|
optimizeLevel = 4;
|
||||||
command.add("-dPDFSETTINGS=/ebook");
|
}
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
command.add("-dPDFSETTINGS=/printer");
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
command.add("-dPDFSETTINGS=/prepress");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
command.add("-dPDFSETTINGS=/default");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-dNOPAUSE");
|
boolean sizeMet = expectedOutputSize == 0L;
|
||||||
command.add("-dQUIET");
|
while (!sizeMet && optimizeLevel <= 5) {
|
||||||
command.add("-dBATCH");
|
// Prepare the Ghostscript command
|
||||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
List<String> command = new ArrayList<>();
|
||||||
command.add(tempInputFile.toString());
|
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) {
|
if (expectedOutputSize != null) {
|
||||||
long outputFileSize = Files.size(tempOutputFile);
|
long outputFileSize = Files.size(tempOutputFile);
|
||||||
if (outputFileSize > expectedOutputSize) {
|
if (outputFileSize > expectedOutputSize) {
|
||||||
try (PDDocument doc = PDDocument.load(new File(tempOutputFile.toString()))) {
|
try (PDDocument doc = PDDocument.load(new File(tempOutputFile.toString()))) {
|
||||||
|
long previousFileSize = 0;
|
||||||
double scaleFactor = 1.0;
|
double scaleFactor = 1.0;
|
||||||
while (true) {
|
while (true) {
|
||||||
for (PDPage page : doc.getPages()) {
|
for (PDPage page : doc.getPages()) {
|
||||||
@ -142,14 +185,21 @@ public class CompressController {
|
|||||||
// save the document to tempOutputFile again
|
// save the document to tempOutputFile again
|
||||||
doc.save(tempOutputFile.toString());
|
doc.save(tempOutputFile.toString());
|
||||||
|
|
||||||
|
long currentSize = Files.size(tempOutputFile);
|
||||||
// Check if the overall PDF size is still larger than expectedOutputSize
|
// 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
|
// The file is still too large, reduce scaleFactor and try again
|
||||||
scaleFactor *= 0.9; // reduce scaleFactor by 10%
|
scaleFactor *= 0.9; // reduce scaleFactor by 10%
|
||||||
// Avoid scaleFactor being too small, causing the image to shrink to 0
|
// Avoid scaleFactor being too small, causing the image to shrink to 0
|
||||||
if(scaleFactor < 0.1){
|
if(scaleFactor < 0.2 || previousFileSize == currentSize){
|
||||||
throw new RuntimeException("Could not reach the desired size without excessively degrading image quality");
|
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 {
|
} else {
|
||||||
// The file is small enough, break the loop
|
// The file is small enough, break the loop
|
||||||
break;
|
break;
|
||||||
|
@ -22,6 +22,7 @@ import java.security.cert.X509Certificate;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
@ -287,4 +288,30 @@ public class PdfUtils {
|
|||||||
|
|
||||||
return PdfUtils.boasToWebResponse(baos, docName);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -224,12 +224,7 @@ compress.title=Compress
|
|||||||
compress.header=Compress PDF
|
compress.header=Compress PDF
|
||||||
compress.credit=This service uses OCRmyPDF for PDF Compress/Optimisation.
|
compress.credit=This service uses OCRmyPDF for PDF Compress/Optimisation.
|
||||||
compress.selectText.1=Optimization level:
|
compress.selectText.1=Optimization level:
|
||||||
compress.selectText.2=0 (No optimization)
|
compress.selectText.2=Expected PDF Size (e.g. 100MB, 25KB, 500B)
|
||||||
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.submit=Compress
|
compress.submit=Compress
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,11 @@ body {
|
|||||||
background-color: rgb(var(--body-background-color)) !important;
|
background-color: rgb(var(--body-background-color)) !important;
|
||||||
color: rgb(var(--base-font-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 {
|
.dark-card {
|
||||||
background-color: rgb(var(--body-background-color)) !important;
|
background-color: rgb(var(--body-background-color)) !important;
|
||||||
|
@ -71,6 +71,11 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
|
|||||||
.favorite-icon img {
|
.favorite-icon img {
|
||||||
filter: brightness(0);
|
filter: brightness(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jumbotron {
|
||||||
|
padding: 3rem 3rem; /* Reduce vertical padding */
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -17,24 +17,29 @@
|
|||||||
<h2 th:text="#{compress.header}"></h2>
|
<h2 th:text="#{compress.header}"></h2>
|
||||||
<form action="#" th:action="@{/compress-pdf}" method="post" enctype="multipart/form-data">
|
<form action="#" th:action="@{/compress-pdf}" method="post" enctype="multipart/form-data">
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||||
<div>
|
<div class="card mb-3">
|
||||||
<label for="optimizeLevel" th:text="#{compress.selectText.1}"></label>
|
<div class="card-body">
|
||||||
<select name="optimizeLevel" id="optimizeLevel">
|
<h4>Manual Mode - From 1 to 5</h4>
|
||||||
<option value="0" th:text="#{compress.selectText.2}"></option>
|
<label for="optimizeLevel" th:text="#{compress.selectText.1}"></label>
|
||||||
<option value="1" selected th:text="#{compress.selectText.3}"></option>
|
<select name="optimizeLevel" id="optimizeLevel" class="form-control">
|
||||||
<option value="2" th:text="#{compress.selectText.4}"></option>
|
<option value="1">1</option>
|
||||||
<option value="3" th:text="#{compress.selectText.5}"></option>
|
<option value="2" selected>2</option>
|
||||||
</select>
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="expectedOutputSize" th:text="#{compress.selectText.8}"></label>
|
<div class="card mb-3">
|
||||||
<input type="number" name="expectedOutputSize" id="expectedOutputSize" min="1">
|
<div class="card-body">
|
||||||
|
<h4>Auto mode - Auto adjusts quality to get PDF to exact size</h4>
|
||||||
|
<label for="expectedOutputSize" th:text="#{compress.selectText.2}"></label>
|
||||||
|
<input type="text" name="expectedOutputSize" id="expectedOutputSize" min="1" class="form-control">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mt-3" th:text="#{compress.credit}"></p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user