compress
This commit is contained in:
parent
f8c855eab1
commit
87cd6dfb54
3 changed files with 166 additions and 67 deletions
37
.github/workflows/push-docker.yml
vendored
37
.github/workflows/push-docker.yml
vendored
|
@ -50,13 +50,13 @@ jobs:
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4.4.0
|
uses: docker/metadata-action@v4.4.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ github.repository_owner }}/s-pdf
|
ghcr.io/${{ github.repository_owner }}/s-pdf
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.versionNumber.outputs.versionNumber }}
|
${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
uses: docker/setup-qemu-action@v2.1.0
|
||||||
|
@ -76,6 +76,22 @@ jobs:
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
- name: Generate tags
|
||||||
|
id: meta2
|
||||||
|
uses: docker/metadata-action@v4.4.0
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
ghcr.io/${{ github.repository_owner }}/s-pdf
|
||||||
|
tags: |
|
||||||
|
${{ steps.versionNumber.outputs.versionNumber }}-ultra-light
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Convert repository owner to lowercase
|
||||||
|
id: repoowner
|
||||||
|
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultralite
|
- name: Build and push Dockerfile-ultralite
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
|
@ -84,12 +100,15 @@ jobs:
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
tags: ${{ steps.meta2.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta2.outputs.labels }}
|
||||||
tags: |
|
tags: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf:ultra-light-latest
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf:ultra-light-latest
|
||||||
ghcr.io/${{ github.repository_owner }}/s-pdf:ultra-light-latest
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf:ultra-light-latest
|
||||||
labels: |
|
labels: |
|
||||||
${{ steps.meta.outputs.labels }}
|
${{ steps.meta2.outputs.labels }}
|
||||||
type=raw,value=ultra-light-latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=ultra-light-latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=ultra-light-alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=ultra-light-alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
package stirling.software.SPDF.controller.api.other;
|
package stirling.software.SPDF.controller.api.other;
|
||||||
|
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.stream.MemoryCacheImageOutputStream;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
@ -20,31 +36,20 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class CompressController {
|
public class CompressController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
|
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||||
@Operation(
|
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters.")
|
||||||
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")
|
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
|
||||||
@Parameter(description = "The input PDF file to be optimized.", required = true)
|
@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 = {
|
||||||
MultipartFile inputFile,
|
"0", "1", "2", "3" }), example = "1") int optimizeLevel,
|
||||||
@RequestParam("optimizeLevel")
|
@RequestParam("expectedOutputSize") @Parameter(description = "The expected output size in bytes.", required = false) Long expectedOutputSize)
|
||||||
@Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.",
|
throws IOException, InterruptedException {
|
||||||
schema = @Schema(allowableValues = {"0", "1", "2", "3"}), example = "1")
|
|
||||||
int optimizeLevel,
|
|
||||||
@RequestParam(name = "fastWebView", required = false)
|
|
||||||
@Parameter(description = "If true, optimize the PDF for fast web view. This increases the file size by about 25%.", example = "false")
|
|
||||||
Boolean fastWebView,
|
|
||||||
@RequestParam(name = "jbig2Lossy", required = false)
|
|
||||||
@Parameter(description = "If true, apply lossy JB2 compression to the PDF file.", example = "false")
|
|
||||||
Boolean jbig2Lossy)
|
|
||||||
throws IOException, InterruptedException {
|
|
||||||
|
|
||||||
// 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");
|
||||||
|
@ -53,31 +58,109 @@ public class CompressController {
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Prepare the OCRmyPDF command
|
// Prepare the Ghostscript command
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("ocrmypdf");
|
command.add("gs");
|
||||||
command.add("--skip-text");
|
command.add("-sDEVICE=pdfwrite");
|
||||||
command.add("--tesseract-timeout=0");
|
command.add("-dCompatibilityLevel=1.4");
|
||||||
command.add("--optimize");
|
|
||||||
command.add(String.valueOf(optimizeLevel));
|
|
||||||
command.add("--output-type");
|
|
||||||
command.add("pdf");
|
|
||||||
|
|
||||||
if (fastWebView != null && fastWebView) {
|
switch (optimizeLevel) {
|
||||||
long fileSize = inputFile.getSize();
|
case 0:
|
||||||
long fastWebViewSize = (long) (fileSize * 1.25); // 25% higher than file size
|
command.add("-dPDFSETTINGS=/default");
|
||||||
command.add("--fast-web-view");
|
break;
|
||||||
command.add(String.valueOf(fastWebViewSize));
|
case 1:
|
||||||
}
|
command.add("-dPDFSETTINGS=/ebook");
|
||||||
|
break;
|
||||||
if (jbig2Lossy != null && jbig2Lossy) {
|
case 2:
|
||||||
command.add("--jbig2-lossy");
|
command.add("-dPDFSETTINGS=/printer");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
command.add("-dPDFSETTINGS=/prepress");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
command.add("-dPDFSETTINGS=/default");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command.add("-dNOPAUSE");
|
||||||
|
command.add("-dQUIET");
|
||||||
|
command.add("-dBATCH");
|
||||||
|
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
command.add(tempOutputFile.toString());
|
|
||||||
|
|
||||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
|
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
if (expectedOutputSize != null) {
|
||||||
|
long outputFileSize = Files.size(tempOutputFile);
|
||||||
|
if (outputFileSize > expectedOutputSize) {
|
||||||
|
try (PDDocument doc = PDDocument.load(new File(tempOutputFile.toString()))) {
|
||||||
|
|
||||||
|
double scaleFactor = 1.0;
|
||||||
|
while (true) {
|
||||||
|
for (PDPage page : doc.getPages()) {
|
||||||
|
PDResources res = page.getResources();
|
||||||
|
|
||||||
|
for (COSName name : res.getXObjectNames()) {
|
||||||
|
PDXObject xobj = res.getXObject(name);
|
||||||
|
if (xobj instanceof PDImageXObject) {
|
||||||
|
PDImageXObject image = (PDImageXObject) xobj;
|
||||||
|
|
||||||
|
// Get the image in BufferedImage format
|
||||||
|
BufferedImage bufferedImage = image.getImage();
|
||||||
|
|
||||||
|
// Calculate the new dimensions
|
||||||
|
int newWidth = (int)(bufferedImage.getWidth() * scaleFactor);
|
||||||
|
int newHeight = (int)(bufferedImage.getHeight() * scaleFactor);
|
||||||
|
|
||||||
|
// If the new dimensions are zero, skip this iteration
|
||||||
|
if (newWidth == 0 || newHeight == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, proceed with the scaling
|
||||||
|
Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||||
|
|
||||||
|
// Convert the scaled image back to a BufferedImage
|
||||||
|
BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||||
|
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
|
||||||
|
|
||||||
|
// Compress the scaled image
|
||||||
|
ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
||||||
|
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||||
|
compressedImageStream.close();
|
||||||
|
|
||||||
|
// Convert compressed image back to PDImageXObject
|
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
|
||||||
|
PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString());
|
||||||
|
|
||||||
|
// Replace the image in the resources with the compressed version
|
||||||
|
res.put(name, compressedImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the document to tempOutputFile again
|
||||||
|
doc.save(tempOutputFile.toString());
|
||||||
|
|
||||||
|
// Check if the overall PDF size is still larger than expectedOutputSize
|
||||||
|
if (Files.size(tempOutputFile) > expectedOutputSize) {
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The file is small enough, break the loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
|
@ -11,31 +11,28 @@
|
||||||
<div id="content-wrap">
|
<div id="content-wrap">
|
||||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||||
<br> <br>
|
<br> <br>
|
||||||
<div class="container">
|
<div class="container">R
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<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>
|
||||||
<label for="optimizeLevel" th:text="#{compress.selectText.1}"></label>
|
<label for="optimizeLevel" th:text="#{compress.selectText.1}"></label>
|
||||||
<select name="optimizeLevel" id="optimizeLevel">
|
<select name="optimizeLevel" id="optimizeLevel">
|
||||||
<option value="0" th:text="#{compress.selectText.2}"></option>
|
<option value="0" th:text="#{compress.selectText.2}"></option>
|
||||||
<option value="1" selected th:text="#{compress.selectText.3}"></option>
|
<option value="1" selected th:text="#{compress.selectText.3}"></option>
|
||||||
<option value="2" th:text="#{compress.selectText.4}"></option>
|
<option value="2" th:text="#{compress.selectText.4}"></option>
|
||||||
<option value="3" th:text="#{compress.selectText.5}"></option>
|
<option value="3" th:text="#{compress.selectText.5}"></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="checkbox" name="fastWebView" id="fastWebView">
|
<label for="expectedOutputSize" th:text="#{compress.selectText.8}"></label>
|
||||||
<label for="fastWebView" th:text="#{compress.selectText.6}"></label>
|
<input type="number" name="expectedOutputSize" id="expectedOutputSize" min="1">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
|
||||||
<input type="checkbox" name="jbig2Lossy" id="jbig2Lossy">
|
</form>
|
||||||
<label for="jbig2Lossy" th:text="#{compress.selectText.7}"></label>
|
|
||||||
</div>
|
|
||||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
|
|
||||||
</form>
|
|
||||||
<p class="mt-3" th:text="#{compress.credit}"></p>
|
<p class="mt-3" th:text="#{compress.credit}"></p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue