This commit is contained in:
PingLin8888 2025-08-10 23:13:37 +08:00 committed by GitHub
commit ee89c7428d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 198 additions and 45 deletions

View File

@ -4,8 +4,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
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.*;
import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@ -31,8 +30,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.SplitTypes;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -48,20 +49,30 @@ public class SplitPdfBySectionsController {
summary = "Split PDF pages into smaller sections", summary = "Split PDF pages into smaller sections",
description = description =
"Split each page of a PDF into smaller sections based on the user's choice" "Split each page of a PDF into smaller sections based on the user's choice"
+ " (halves, thirds, quarters, etc.), both vertically and horizontally." + " which page to split, and how to split"
+ " ( halves, thirds, quarters, etc.), both vertically and horizontally."
+ " Input:PDF Output:ZIP-PDF Type:SISO") + " Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
throws Exception { throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String pageNumbers = request.getPageNumbers();
SplitTypes splitMode =
Optional.ofNullable(request.getSplitMode())
.map(SplitTypes::valueOf)
.orElse(SplitTypes.SPLIT_ALL);
PDDocument sourceDocument = pdfDocumentFactory.load(file); PDDocument sourceDocument = pdfDocumentFactory.load(file);
Set<Integer> pagesToSplit =
getPagesToSplit(pageNumbers, splitMode, sourceDocument.getNumberOfPages());
// Process the PDF based on split parameters // Process the PDF based on split parameters
int horiz = request.getHorizontalDivisions() + 1; int horiz = request.getHorizontalDivisions() + 1;
int verti = request.getVerticalDivisions() + 1; int verti = request.getVerticalDivisions() + 1;
boolean merge = Boolean.TRUE.equals(request.getMerge()); boolean merge = Boolean.TRUE.equals(request.getMerge());
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz); List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz, pagesToSplit);
String filename = String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
@ -109,52 +120,111 @@ public class SplitPdfBySectionsController {
} }
} }
// Based on the mode, get the pages that need to be split and return the pages set
private Set<Integer> getPagesToSplit(String pageNumbers, SplitTypes splitMode, int totalPages) {
Set<Integer> pagesToSplit = new HashSet<>();
switch (splitMode) {
case CUSTOM:
if (pageNumbers == null || pageNumbers.isBlank()) {
throw new IllegalArgumentException("Custom mode requires page numbers input.");
}
String[] pageOrderArr = pageNumbers.split(",");
List<Integer> pageListToSplit =
GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
pagesToSplit.addAll(pageListToSplit);
break;
case SPLIT_ALL:
for (int i = 0; i < totalPages; i++) {
pagesToSplit.add(i);
}
break;
case SPLIT_ALL_EXCEPT_FIRST:
for (int i = 1; i < totalPages; i++) {
pagesToSplit.add(i);
}
break;
case SPLIT_ALL_EXCEPT_LAST:
for (int i = 0; i < totalPages - 1; i++) {
pagesToSplit.add(i);
}
break;
case SPLIT_ALL_EXCEPT_FIRST_AND_LAST:
for (int i = 1; i < totalPages - 1; i++) {
pagesToSplit.add(i);
}
break;
default:
throw new IllegalArgumentException("Unsupported split mode: " + splitMode);
}
return pagesToSplit;
}
public List<PDDocument> splitPdfPages( public List<PDDocument> splitPdfPages(
PDDocument document, int horizontalDivisions, int verticalDivisions) PDDocument document,
int horizontalDivisions,
int verticalDivisions,
Set<Integer> pagesToSplit)
throws IOException { throws IOException {
List<PDDocument> splitDocuments = new ArrayList<>(); List<PDDocument> splitDocuments = new ArrayList<>();
int pageIndex = 0;
for (PDPage originalPage : document.getPages()) { for (PDPage originalPage : document.getPages()) {
PDRectangle originalMediaBox = originalPage.getMediaBox(); // If current page is not to split, add it to the splitDocuments directly.
float width = originalMediaBox.getWidth(); if (!pagesToSplit.contains(pageIndex)) {
float height = originalMediaBox.getHeight(); PDDocument newDoc = pdfDocumentFactory.createNewDocument();
float subPageWidth = width / horizontalDivisions; newDoc.addPage(originalPage);
float subPageHeight = height / verticalDivisions; splitDocuments.add(newDoc);
} else {
// Otherwise, split current page.
PDRectangle originalMediaBox = originalPage.getMediaBox();
float width = originalMediaBox.getWidth();
float height = originalMediaBox.getHeight();
float subPageWidth = width / horizontalDivisions;
float subPageHeight = height / verticalDivisions;
LayerUtility layerUtility = new LayerUtility(document); LayerUtility layerUtility = new LayerUtility(document);
for (int i = 0; i < horizontalDivisions; i++) { for (int i = 0; i < horizontalDivisions; i++) {
for (int j = 0; j < verticalDivisions; j++) { for (int j = 0; j < verticalDivisions; j++) {
PDDocument subDoc = new PDDocument(); PDDocument subDoc = new PDDocument();
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight)); PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
subDoc.addPage(subPage); subDoc.addPage(subPage);
PDFormXObject form = PDFormXObject form =
layerUtility.importPageAsForm( layerUtility.importPageAsForm(
document, document.getPages().indexOf(originalPage)); document, document.getPages().indexOf(originalPage));
try (PDPageContentStream contentStream = try (PDPageContentStream contentStream =
new PDPageContentStream( new PDPageContentStream(
subDoc, subPage, AppendMode.APPEND, true, true)) { subDoc, subPage, AppendMode.APPEND, true, true)) {
// Set clipping area and position // Set clipping area and position
float translateX = -subPageWidth * i; float translateX = -subPageWidth * i;
// float translateY = height - subPageHeight * (verticalDivisions - j); // float translateY = height - subPageHeight * (verticalDivisions - j);
float translateY = -subPageHeight * (verticalDivisions - 1 - j); float translateY = -subPageHeight * (verticalDivisions - 1 - j);
contentStream.saveGraphicsState(); contentStream.saveGraphicsState();
contentStream.addRect(0, 0, subPageWidth, subPageHeight); contentStream.addRect(0, 0, subPageWidth, subPageHeight);
contentStream.clip(); contentStream.clip();
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY)); contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
// Draw the form // Draw the form
contentStream.drawForm(form); contentStream.drawForm(form);
contentStream.restoreGraphicsState(); contentStream.restoreGraphicsState();
}
splitDocuments.add(subDoc);
} }
splitDocuments.add(subDoc);
} }
} }
pageIndex++;
} }
return splitDocuments; return splitDocuments;

View File

@ -0,0 +1,9 @@
package stirling.software.SPDF.model;
public enum SplitTypes {
CUSTOM,
SPLIT_ALL_EXCEPT_FIRST_AND_LAST,
SPLIT_ALL_EXCEPT_FIRST,
SPLIT_ALL_EXCEPT_LAST,
SPLIT_ALL
}

View File

@ -5,11 +5,29 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.SplitTypes;
import stirling.software.common.model.api.PDFFile; import stirling.software.common.model.api.PDFFile;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class SplitPdfBySectionsRequest extends PDFFile { public class SplitPdfBySectionsRequest extends PDFFile {
@Schema(
description = "Pages to be split by section",
defaultValue = "all",
requiredMode = Schema.RequiredMode.REQUIRED)
private String pageNumbers;
@Schema(
implementation = SplitTypes.class,
description =
"Modes for page split. Valid values are:\n"
+ "SPLIT_ALL_EXCEPT_FIRST_AND_LAST: Splits all except the first and the last pages.\n"
+ "SPLIT_ALL_EXCEPT_FIRST: Splits all except the first page.\n"
+ "SPLIT_ALL_EXCEPT_LAST: Splits all except the last page.\n"
+ "SPLIT_ALL: Splits all pages.\n"
+ "CUSTOM: Custom split.\n")
private String splitMode;
@Schema( @Schema(
description = "Number of horizontal divisions for each PDF page", description = "Number of horizontal divisions for each PDF page",
defaultValue = "0", defaultValue = "0",

View File

@ -838,8 +838,8 @@ home.overlay-pdfs.desc=Overlays PDFs on-top of another PDF
overlay-pdfs.tags=Overlay overlay-pdfs.tags=Overlay
home.split-by-sections.title=Split PDF by Sections home.split-by-sections.title=Split PDF by Sections
home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical sections home.split-by-sections.desc=Split the content of specified PDF pages into smaller horizontal and vertical sections.
split-by-sections.tags=Section Split, Divide, Customize,Customise split-by-sections.tags=Section Split, Divide, Customize, Customise
home.AddStampRequest.title=Add Stamp to PDF home.AddStampRequest.title=Add Stamp to PDF
home.AddStampRequest.desc=Add text or add image stamps at set locations home.AddStampRequest.desc=Add text or add image stamps at set locations
@ -1619,6 +1619,15 @@ split-by-sections.horizontal.placeholder=Enter number of horizontal divisions
split-by-sections.vertical.placeholder=Enter number of vertical divisions split-by-sections.vertical.placeholder=Enter number of vertical divisions
split-by-sections.submit=Split PDF split-by-sections.submit=Split PDF
split-by-sections.merge=Merge Into One PDF split-by-sections.merge=Merge Into One PDF
split-by-sections.pageToSplit=Pages to split (Enter a comma-separated list of page numbers) :
split-by-sections.pageToSplit.placeholder=(e.g. 1,2,6)
split-by-sections.mode=Split Mode
split-by-sections.mode.1=Split All Except First & Last
split-by-sections.mode.2=Split All Except First
split-by-sections.mode.3=Split All Except Last
split-by-sections.mode.4=Split All Pages
split-by-sections.mode.5=Custom: Specify Pages
#printFile #printFile

View File

@ -8,3 +8,6 @@
position: absolute; position: absolute;
background-color: red; /* Line color */ background-color: red; /* Line color */
} }
#pageToSplitSection {
display: none;
}

View File

@ -42,22 +42,29 @@
<option value="REMOVE_FIRST_AND_LAST" th:text="#{pdfOrganiser.mode.9}">Remove First and Last</option> <option value="REMOVE_FIRST_AND_LAST" th:text="#{pdfOrganiser.mode.9}">Remove First and Last</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3" id="pageToOrganizeSection">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label> <label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageNumbers" <input type="text" class="form-control" id="pageOrder" name="pageNumbers"
th:placeholder="#{pdfOrganiser.placeholder}" required> th:placeholder="#{pdfOrganiser.placeholder}">
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button>
</form> </form>
<script> <script>
document.getElementById('customMode').addEventListener('change', function () { // Only display the page input field when split mode is custom.
var pageOrderInput = document.getElementById('pageOrder'); function togglePageInput() {
if (this.value === "" || this.value === "DUPLICATE") { const mode = document.getElementById('customMode').value;
pageOrderInput.disabled = false; const pageInputSection = document.getElementById('pageToOrganizeSection');
if (mode === "") {
pageInputSection.style.display = "block";
document.getElementById('pageOrder').setAttribute("required", "true");
} else { } else {
pageOrderInput.disabled = true; pageInputSection.style.display = "none";
document.getElementById('pageOrder').removeAttribute("required");
} }
}); }
document.getElementById('customMode').addEventListener('change', togglePageInput);
window.addEventListener('DOMContentLoaded', togglePageInput);
</script> </script>
</div> </div>
</div> </div>

View File

@ -20,6 +20,21 @@
</div> </div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/split-pdf-by-sections'}"> <form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/split-pdf-by-sections'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label for="splitMode" th:text="#{split-by-sections.mode}">Split Mode</label>
<select class="form-control" id="splitMode" name="splitMode">
<option value="SPLIT_ALL" th:text="#{split-by-sections.mode.4}">Split all</option>
<option value="SPLIT_ALL_EXCEPT_FIRST_AND_LAST" th:text="#{split-by-sections.mode.1}">Split all except first and last</option>
<option value="SPLIT_ALL_EXCEPT_FIRST" th:text="#{split-by-sections.mode.2}">Split all except first</option>
<option value="SPLIT_ALL_EXCEPT_LAST" th:text="#{split-by-sections.mode.3}">Split all except last</option>
<option value="CUSTOM" th:text="#{split-by-sections.mode.5}">Specify pages to split</option>
</select>
</div>
<div class="mb-3" id="pageToSplitSection">
<label for="pageToSplit" th:text="#{split-by-sections.pageToSplit}"></label>
<input type="text" class="form-control" id="pageToSplit" name="pageNumbers"
th:placeholder="#{split-by-sections.pageToSplit.placeholder}">
</div>
<label for="horizontalDivisions" th:text="#{split-by-sections.horizontal.label}">Horizontal Divisions</label> <label for="horizontalDivisions" th:text="#{split-by-sections.horizontal.label}">Horizontal Divisions</label>
<input type="number" id="horizontalDivisions" name="horizontalDivisions" class="form-control" min="0" max="300" value="0" required th:placeholder="#{split-by-sections.horizontal.placeholder}"> <input type="number" id="horizontalDivisions" name="horizontalDivisions" class="form-control" min="0" max="300" value="0" required th:placeholder="#{split-by-sections.horizontal.placeholder}">
<br> <br>
@ -74,6 +89,28 @@
// Initial draw // Initial draw
updateVisualAid(); updateVisualAid();
// Removes all whitespace characters (spaces, tabs, newlines) as the user types
document.getElementById('pageToSplit').addEventListener('input', function () {
this.value = this.value.replace(/\s+/g, '');;
});
// Only display the page input field when split mode is custom.
function togglePageInput() {
const mode = document.getElementById('splitMode').value;
const pageInputSection = document.getElementById('pageToSplitSection');
if (mode === "CUSTOM") {
pageInputSection.style.display = "block";
document.getElementById('pageToSplit').setAttribute("required", "true");
} else {
pageInputSection.style.display = "none";
document.getElementById('pageToSplit').removeAttribute("required");
}
}
document.getElementById('splitMode').addEventListener('change', togglePageInput);
window.addEventListener('DOMContentLoaded', togglePageInput);
</script> </script>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{split-by-sections.submit}">Submit</button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{split-by-sections.submit}">Submit</button>