impl migration to pdfium for decompress
This commit is contained in:
+35
-26
@@ -1,5 +1,6 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashSet;
|
||||
@@ -30,6 +31,7 @@ import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@@ -51,23 +53,41 @@ public class DecompressPdfController {
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
||||
// Process all objects in document
|
||||
processAllObjects(document);
|
||||
|
||||
// Save with explicit no compression to a temp file
|
||||
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
|
||||
try {
|
||||
document.save(tempOut.getFile(), CompressParameters.NO_COMPRESSION);
|
||||
} catch (IOException e) {
|
||||
tempOut.close();
|
||||
throw e;
|
||||
// JPDFium fast pre-validate: catches corrupt PDFs cheaply before the expensive PDFBox walk.
|
||||
// JPDFium's FPDF_SaveAsCopy has no "uncompress streams" flag, so PDFBox does the actual
|
||||
// work.
|
||||
File inputTemp = null;
|
||||
try {
|
||||
inputTemp = tempFileManager.convertMultipartFileToFile(file);
|
||||
try (PdfDocument ignored = PdfDocument.open(inputTemp.toPath())) {
|
||||
// pre-validate only
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Return the PDF as a streaming response
|
||||
return WebResponseUtils.pdfFileToWebResponse(
|
||||
tempOut,
|
||||
GeneralUtils.generateFilename(file.getOriginalFilename(), "_decompressed.pdf"));
|
||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
||||
// Walk every object and strip stream filters so PDFBox writes raw bytes
|
||||
processAllObjects(document);
|
||||
|
||||
// Hybrid fallback: PDFium cannot save uncompressed, so use PDFBox NO_COMPRESSION
|
||||
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
|
||||
try {
|
||||
document.save(tempOut.getFile(), CompressParameters.NO_COMPRESSION);
|
||||
} catch (IOException e) {
|
||||
tempOut.close();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfFileToWebResponse(
|
||||
tempOut,
|
||||
GeneralUtils.generateFilename(
|
||||
file.getOriginalFilename(), "_decompressed.pdf"));
|
||||
}
|
||||
} finally {
|
||||
if (inputTemp != null) {
|
||||
tempFileManager.deleteTempFile(inputTemp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +95,6 @@ public class DecompressPdfController {
|
||||
Set<COSBase> processed = new HashSet<>();
|
||||
COSDocument cosDoc = document.getDocument();
|
||||
|
||||
// Process all objects in the document
|
||||
for (COSObjectKey key : cosDoc.getXrefTable().keySet()) {
|
||||
COSObject obj = cosDoc.getObjectFromPool(key);
|
||||
processObject(obj, processed);
|
||||
@@ -83,7 +102,6 @@ public class DecompressPdfController {
|
||||
}
|
||||
|
||||
private void processObject(COSBase obj, Set<COSBase> processed) {
|
||||
// Skip null objects or already processed objects to avoid infinite recursion
|
||||
if (obj == null || processed.contains(obj)) return;
|
||||
processed.add(obj);
|
||||
|
||||
@@ -97,19 +115,16 @@ public class DecompressPdfController {
|
||||
}
|
||||
|
||||
private void processDictionary(COSDictionary dict, Set<COSBase> processed) {
|
||||
// Process all dictionary entries
|
||||
for (COSName key : dict.keySet()) {
|
||||
processObject(dict.getDictionaryObject(key), processed);
|
||||
}
|
||||
|
||||
// If this is a stream, decompress it
|
||||
if (dict instanceof COSStream stream) {
|
||||
decompressStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private void processArray(COSArray array, Set<COSBase> processed) {
|
||||
// Process all array elements
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
processObject(array.get(i), processed);
|
||||
}
|
||||
@@ -119,33 +134,27 @@ public class DecompressPdfController {
|
||||
try {
|
||||
log.debug("Processing stream: {}", stream);
|
||||
|
||||
// Only remove filter information if it exists
|
||||
if (stream.containsKey(COSName.FILTER)
|
||||
|| stream.containsKey(COSName.DECODE_PARMS)
|
||||
|| stream.containsKey(COSName.D)) {
|
||||
|
||||
// Read the decompressed content first
|
||||
byte[] decompressedBytes;
|
||||
try (COSInputStream is = stream.createInputStream()) {
|
||||
decompressedBytes = IOUtils.toByteArray(is);
|
||||
}
|
||||
|
||||
// Now remove filter information
|
||||
stream.removeItem(COSName.FILTER);
|
||||
stream.removeItem(COSName.DECODE_PARMS);
|
||||
stream.removeItem(COSName.D);
|
||||
|
||||
// Write the raw content back
|
||||
try (OutputStream out = stream.createRawOutputStream()) {
|
||||
out.write(decompressedBytes);
|
||||
}
|
||||
|
||||
// Set the Length to reflect the new stream size
|
||||
stream.setInt(COSName.LENGTH, decompressedBytes.length);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("stream decompression", e);
|
||||
// Continue processing other streams even if this one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -1,6 +1,7 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -68,6 +69,16 @@ class DecompressPdfControllerTest {
|
||||
lenient().when(tf.getPath()).thenReturn(f.toPath());
|
||||
return tf;
|
||||
});
|
||||
// Stub multipart-to-file conversion for the JPDFium pre-validate step
|
||||
lenient()
|
||||
.when(tempFileManager.convertMultipartFileToFile(any()))
|
||||
.thenAnswer(
|
||||
inv -> {
|
||||
org.springframework.web.multipart.MultipartFile mf = inv.getArgument(0);
|
||||
File f = Files.createTempFile("input", ".pdf").toFile();
|
||||
java.nio.file.Files.write(f.toPath(), mf.getBytes());
|
||||
return f;
|
||||
});
|
||||
}
|
||||
|
||||
private MockMultipartFile createRealPdf(String content) throws IOException {
|
||||
@@ -209,6 +220,33 @@ class DecompressPdfControllerTest {
|
||||
assertThat(drainBody(response).length).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void decompressPdf_oracleNoFilterEntries() throws IOException {
|
||||
// PDFBox oracle: load the decompressed output and verify every COSStream has no /Filter
|
||||
MockMultipartFile file = createRealPdf("Oracle decompression check");
|
||||
PDFFile request = new PDFFile();
|
||||
request.setFileInput(file);
|
||||
|
||||
PDDocument doc = Loader.loadPDF(file.getBytes());
|
||||
when(pdfDocumentFactory.load(file)).thenReturn(doc);
|
||||
|
||||
ResponseEntity<Resource> response = controller.decompressPdf(request);
|
||||
byte[] body = drainBody(response);
|
||||
|
||||
try (PDDocument result = Loader.loadPDF(body)) {
|
||||
org.apache.pdfbox.cos.COSDocument cosDoc = result.getDocument();
|
||||
for (org.apache.pdfbox.cos.COSObjectKey key : cosDoc.getXrefTable().keySet()) {
|
||||
org.apache.pdfbox.cos.COSObject obj = cosDoc.getObjectFromPool(key);
|
||||
org.apache.pdfbox.cos.COSBase base = obj.getObject();
|
||||
if (base instanceof org.apache.pdfbox.cos.COSStream stream) {
|
||||
assertThat(stream.containsKey(org.apache.pdfbox.cos.COSName.FILTER))
|
||||
.as("stream %s should have no /Filter", key)
|
||||
.isFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void decompressPdf_returnsOkContentType() throws IOException {
|
||||
MockMultipartFile file = createRealPdf("test");
|
||||
|
||||
Reference in New Issue
Block a user