mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Code refactoring
Backend: - Now using createNewDocumentBasedOnOldDocument - Replaced LayerUtility importPageAsForm with manual cloning using PDFCloneUtility to avoid rotation issues - Wrapped original content streams into PDFormXObject - Updated getRemovalZonesForPage to correctly handle page rotation Frontend: - Using viewport instead of manual scale to handle rotation
This commit is contained in:
parent
dc1050cee5
commit
e30756491b
@ -1,16 +1,23 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||
import org.apache.pdfbox.cos.COSArray;
|
||||
import org.apache.pdfbox.cos.COSBase;
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
import org.apache.pdfbox.cos.COSStream;
|
||||
import org.apache.pdfbox.multipdf.PDFCloneUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@ -48,15 +55,12 @@ public class RemoveHeaderFooterController {
|
||||
throws IOException {
|
||||
|
||||
MultipartFile pdfFile = form.getFileInput();
|
||||
|
||||
PDDocument sourceDoc = pdfDocumentFactory.load(pdfFile);
|
||||
PDDocument newDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc);
|
||||
String pagesToDelete = form.getPages();
|
||||
List<Integer> pagesToRemove = new ArrayList<>();
|
||||
PDDocument sourceDoc = pdfDocumentFactory.load(pdfFile);
|
||||
PDDocument newDoc = new PDDocument();
|
||||
LayerUtility layerUtility = new LayerUtility(newDoc);
|
||||
|
||||
String sufix;
|
||||
// Respond with a message
|
||||
|
||||
if (form.isRemoveHeader()) {
|
||||
if (form.isRemoveFooter()) {
|
||||
sufix = "_removed_header_footer.pdf";
|
||||
@ -64,8 +68,7 @@ public class RemoveHeaderFooterController {
|
||||
} else if (form.isRemoveFooter()) {
|
||||
sufix = "_removed_footer.pdf";
|
||||
} else {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("No header or footer removal options selected".getBytes());
|
||||
throw new IllegalArgumentException("Header and/or footer removal must be selected");
|
||||
}
|
||||
|
||||
if (pagesToDelete == null || pagesToDelete.isEmpty()) {
|
||||
@ -81,42 +84,70 @@ public class RemoveHeaderFooterController {
|
||||
Collections.sort(pagesToRemove);
|
||||
}
|
||||
|
||||
// Used to clone the old PDF document to a new one, preserving the original document
|
||||
// structure and properties
|
||||
PDFCloneUtility cloner;
|
||||
try {
|
||||
Constructor<PDFCloneUtility> constructor =
|
||||
PDFCloneUtility.class.getDeclaredConstructor(PDDocument.class);
|
||||
// Enable access to protected constructor
|
||||
constructor.setAccessible(true);
|
||||
cloner = constructor.newInstance(newDoc);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to clone the old PDF document to a new one: ", e);
|
||||
}
|
||||
|
||||
for (int pageIndex = 0; pageIndex < sourceDoc.getNumberOfPages(); pageIndex++) {
|
||||
PDPage sourcePage = sourceDoc.getPage(pageIndex);
|
||||
PDRectangle mediaBox = sourcePage.getMediaBox();
|
||||
|
||||
PDPage newPage = new PDPage(mediaBox);
|
||||
PDPage newPage =
|
||||
new PDPage(
|
||||
(COSDictionary) cloner.cloneForNewDocument(sourcePage.getCOSObject()));
|
||||
newDoc.addPage(newPage);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(newDoc, newPage, AppendMode.OVERWRITE, true, true)) {
|
||||
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDoc, pageIndex);
|
||||
if (pagesToRemove.contains(pageIndex)) {
|
||||
PDRectangle mediaBox = newPage.getMediaBox();
|
||||
Float[][] zones = getRemovalZonesForPage(form, newPage);
|
||||
|
||||
// Extract original content streams
|
||||
List<PDStream> oldStreams = extractContentStreams(newPage);
|
||||
|
||||
newPage.setContents(new ArrayList<>());
|
||||
|
||||
COSStream combinedStream = new COSStream();
|
||||
try (OutputStream out = combinedStream.createOutputStream()) {
|
||||
for (PDStream stream : oldStreams) {
|
||||
out.write(stream.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
PDFormXObject formXObject = new PDFormXObject(combinedStream);
|
||||
formXObject.setResources(newPage.getResources());
|
||||
formXObject.setBBox(mediaBox);
|
||||
formXObject.setFormType(1); // Required form type
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(
|
||||
newDoc, newPage, AppendMode.OVERWRITE, true, true)) {
|
||||
// Save the current graphics state to restore later
|
||||
cs.saveGraphicsState();
|
||||
|
||||
if (pagesToRemove.contains(pageIndex)) {
|
||||
Float[][] zones = getRemovalZonesForPage(form, sourcePage);
|
||||
if (zones != null && zones.length > 0) {
|
||||
cs.addRect(0, 0, mediaBox.getWidth(), mediaBox.getHeight());
|
||||
// Add rectangles for each zone to remove (header/footer areas)
|
||||
// These will be subtracted from the base rectangle using even-odd clipping
|
||||
// rule
|
||||
for (Float[] zone : zones) {
|
||||
if (zone != null && zone.length == 4) {
|
||||
cs.addRect(zone[0], zone[1], zone[2], zone[3]);
|
||||
}
|
||||
}
|
||||
|
||||
cs.clipEvenOdd();
|
||||
}
|
||||
}
|
||||
|
||||
cs.drawForm(formXObject);
|
||||
// Restore the graphics state to ensure the clipping is applied correctly
|
||||
cs.restoreGraphicsState();
|
||||
}
|
||||
}
|
||||
}
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
newDoc,
|
||||
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||
@ -124,6 +155,23 @@ public class RemoveHeaderFooterController {
|
||||
+ sufix);
|
||||
}
|
||||
|
||||
private List<PDStream> extractContentStreams(PDPage page) {
|
||||
List<PDStream> streams = new ArrayList<>();
|
||||
COSBase contents = page.getCOSObject().getDictionaryObject("Contents");
|
||||
|
||||
if (contents instanceof COSStream cosStream) {
|
||||
streams.add(new PDStream(cosStream));
|
||||
} else if (contents instanceof COSArray cosArray) {
|
||||
for (int i = 0; i < cosArray.size(); i++) {
|
||||
COSBase item = cosArray.get(i);
|
||||
if (item instanceof COSStream itemStream) {
|
||||
streams.add(new PDStream(itemStream));
|
||||
}
|
||||
}
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the zones for the header and footer removal based on the form data and the page.
|
||||
*
|
||||
@ -132,30 +180,60 @@ public class RemoveHeaderFooterController {
|
||||
* @return A 2D array of Float representing the zones to remove.
|
||||
*/
|
||||
private Float[][] getRemovalZonesForPage(RemoveHeaderFooterForm form, PDPage page) {
|
||||
float w = page.getMediaBox().getWidth();
|
||||
float h = page.getMediaBox().getHeight();
|
||||
Float[][] zones = null;
|
||||
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float w = mediaBox.getWidth();
|
||||
float h = mediaBox.getHeight();
|
||||
int rotation = page.getRotation();
|
||||
boolean removeHeader = form.isRemoveHeader();
|
||||
boolean removeFooter = form.isRemoveFooter();
|
||||
zones = new Float[removeHeader && removeFooter ? 2 : 1][];
|
||||
Float[][] zones = new Float[removeHeader && removeFooter ? 2 : 1][];
|
||||
int zoneIdx = 0;
|
||||
|
||||
if (removeHeader) {
|
||||
|
||||
Float headerH = form.getHeaderMargin();
|
||||
if (headerH == -1) {
|
||||
headerH = form.getHeaderCustomValue(); // Default value if 'custom' is specified
|
||||
}
|
||||
zones[0] = new Float[] {0f, h - headerH, w, headerH};
|
||||
|
||||
Float[] rawZone;
|
||||
if (rotation == 90 || rotation == 270) {
|
||||
rawZone = new Float[] {0f, w - headerH, h, headerH};
|
||||
} else {
|
||||
rawZone = new Float[] {0f, h - headerH, w, headerH};
|
||||
}
|
||||
zones[zoneIdx++] = rotateZone(rawZone, mediaBox, rotation);
|
||||
}
|
||||
|
||||
if (removeFooter) {
|
||||
|
||||
Float footerH = form.getFooterMargin();
|
||||
if (footerH == -1) {
|
||||
footerH = form.getFooterCustomValue(); // Default value if 'custom' is specified
|
||||
}
|
||||
zones[zones[0] == null ? 0 : 1] = new Float[] {0f, 0f, w, footerH};
|
||||
}
|
||||
|
||||
Float[] rawZone;
|
||||
if (rotation == 90 || rotation == 270) {
|
||||
rawZone = new Float[] {0f, 0f, h, footerH};
|
||||
} else {
|
||||
rawZone = new Float[] {0f, 0f, w, footerH};
|
||||
}
|
||||
zones[zoneIdx] = rotateZone(rawZone, mediaBox, rotation);
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
private Float[] rotateZone(Float[] zone, PDRectangle mediaBox, int rotation) {
|
||||
float x = zone[0];
|
||||
float y = zone[1];
|
||||
float w = zone[2];
|
||||
float h = zone[3];
|
||||
return switch (rotation) {
|
||||
case 90 -> new Float[] {mediaBox.getWidth() - y - h, x, h, w};
|
||||
case 180 ->
|
||||
new Float[] {mediaBox.getWidth() - x - w, mediaBox.getHeight() - y - h, w, h};
|
||||
case 270 -> new Float[] {y, mediaBox.getHeight() - x - w, h, w};
|
||||
default -> new Float[] {x, y, w, h};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -100,30 +100,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
const page = await loadedPdf.getPage(firstPageNumber);
|
||||
const canvas = document.createElement("canvas");
|
||||
const viewport = page.getViewport({ scale: currentScale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let scale;
|
||||
if (page.rotate === 90 || page.rotate === 270) {
|
||||
canvas.width = page.view[3];
|
||||
canvas.height = page.view[2];
|
||||
scale = canvas.height / page.view[2];
|
||||
} else {
|
||||
canvas.width = page.view[2];
|
||||
canvas.height = page.view[3];
|
||||
scale = canvas.height / page.view[3];
|
||||
}
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: ctx,
|
||||
viewport: page.getViewport({ scale: 1 }),
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
const headerY = mainHeaderMargin * scale;
|
||||
const footerY = canvas.height - mainFooterMargin * scale;
|
||||
const headerY = mainHeaderMargin * currentScale;
|
||||
const footerY = canvas.height - mainFooterMargin * currentScale;
|
||||
|
||||
if (mainHeaderCheckbox.checked) {
|
||||
drawMarginLine(ctx, headerY, canvas.width, "header");
|
||||
@ -180,20 +167,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const viewport = page.getViewport({ scale: currentScale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.margin = '0 auto 16px auto';
|
||||
|
||||
viewer.appendChild(canvas);
|
||||
|
||||
page.render({ canvasContext: ctx, viewport: viewport }).promise.then(() => {
|
||||
if (idx === 0) {
|
||||
pageNumberInput.value = pagesToShow[0];
|
||||
numPagesLabel.textContent = `/ ${pagesToShow.length}`;
|
||||
}
|
||||
|
||||
const scale = canvas.height / page.view[3];
|
||||
const headerY = headerMargin * scale;
|
||||
const footerY = canvas.height - footerMargin * scale;
|
||||
const headerY = headerMargin * currentScale;
|
||||
const footerY = canvas.height - footerMargin * currentScale;
|
||||
ctx.strokeStyle = "red";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
Loading…
Reference in New Issue
Block a user