fixing routing to page

This commit is contained in:
Dario Ghunney Ware 2025-06-12 18:16:45 +01:00
parent 5304381236
commit 0f5c549fb4
7 changed files with 179 additions and 52 deletions

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -8,8 +7,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.PageMode; import org.apache.pdfbox.pdmodel.PageMode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -21,22 +18,26 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.service.PDFAttachmentServiceInterface;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@Slf4j
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@Slf4j
public class AttachmentsController { public class AttachmentsController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final PDFAttachmentServiceInterface pdfAttachmentService;
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments") @PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
@Operation( @Operation(
summary = "Add attachments to PDF", summary = "Add attachments to PDF",
description = "This endpoint adds embedded files (attachments) to a PDF and sets the PageMode to UseAttachments to make them visible. Input:PDF + Files Output:PDF Type:MISO") description =
"This endpoint adds embedded files (attachments) to a PDF and sets the PageMode to UseAttachments to make them visible. Input:PDF + Files Output:PDF Type:MISO")
public ResponseEntity<byte[]> addAttachments( public ResponseEntity<byte[]> addAttachments(
@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("fileInput") MultipartFile pdfFile,
@RequestParam("attachments") List<MultipartFile> attachments) @RequestParam("attachments") List<MultipartFile> attachments)
@ -50,17 +51,14 @@ public class AttachmentsController {
// Create embedded files name tree if it doesn't exist // Create embedded files name tree if it doesn't exist
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
if (efTree == null) { if (efTree == null) {
efTree = new PDEmbeddedFilesNameTreeNode(); efTree = new PDEmbeddedFilesNameTreeNode();
catalog.getNames().setEmbeddedFiles(efTree); catalog.getNames().setEmbeddedFiles(efTree);
} }
// Add each attachment // Add attachments
for (MultipartFile attachment : attachments) { pdfAttachmentService.addAttachment(document, efTree, attachments);
if (attachment != null && !attachment.isEmpty()) {
addEmbeddedFile(document, efTree, attachment);
}
}
// Set PageMode to UseAttachments to show the attachments panel // Set PageMode to UseAttachments to show the attachments panel
catalog.setPageMode(PageMode.USE_ATTACHMENTS); catalog.setPageMode(PageMode.USE_ATTACHMENTS);
@ -69,16 +67,17 @@ public class AttachmentsController {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") + "_with_attachments.pdf"); .replaceFirst("[.][^.]+$", "")
+ "_with_attachments.pdf");
} }
@PostMapping(consumes = "multipart/form-data", value = "/remove-attachments") @PostMapping(consumes = "multipart/form-data", value = "/remove-attachments")
@Operation( @Operation(
summary = "Remove attachments from PDF", summary = "Remove attachments from PDF",
description = "This endpoint removes all embedded files (attachments) from a PDF. Input:PDF Output:PDF Type:SISO") description =
"This endpoint removes all embedded files (attachments) from a PDF. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeAttachments( public ResponseEntity<byte[]> removeAttachments(
@RequestParam("fileInput") MultipartFile pdfFile) @RequestParam("fileInput") MultipartFile pdfFile) throws IOException {
throws IOException {
// Load the PDF document // Load the PDF document
PDDocument document = pdfDocumentFactory.load(pdfFile, true); PDDocument document = pdfDocumentFactory.load(pdfFile, true);
@ -98,35 +97,7 @@ public class AttachmentsController {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") + "_attachments_removed.pdf"); .replaceFirst("[.][^.]+$", "")
} + "_attachments_removed.pdf");
private void addEmbeddedFile(PDDocument document, PDEmbeddedFilesNameTreeNode efTree, MultipartFile file)
throws IOException {
// Create file specification
PDComplexFileSpecification fs = new PDComplexFileSpecification();
fs.setFile(file.getOriginalFilename());
fs.setFileDescription("Embedded file: " + file.getOriginalFilename());
// Create embedded file
PDEmbeddedFile ef = new PDEmbeddedFile(document, new ByteArrayInputStream(file.getBytes()));
ef.setSize((int) file.getSize());
ef.setCreationDate(new java.util.GregorianCalendar());
ef.setModDate(new java.util.GregorianCalendar());
// Set MIME type if available
String contentType = file.getContentType();
if (contentType != null && !contentType.isEmpty()) {
ef.setSubtype(contentType);
}
// Associate embedded file with file specification
fs.setEmbeddedFile(ef);
// Add to the name tree
efTree.setNames(java.util.Collections.singletonMap(file.getOriginalFilename(), fs));
log.info("Added embedded file: {} ({} bytes)", file.getOriginalFilename(), file.getSize());
} }
} }

View File

@ -0,0 +1,81 @@
package stirling.software.SPDF.service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class PDFAttachmentService implements PDFAttachmentServiceInterface {
@Override
public void addAttachment(
PDDocument document,
PDEmbeddedFilesNameTreeNode efTree,
List<MultipartFile> attachments)
throws IOException {
// Get existing names or create new map
Map<String, PDComplexFileSpecification> existingNames = new java.util.HashMap<>();
try {
existingNames = efTree.getNames();
} catch (IOException e) {
log.warn("Could not retrieve existing embedded files, starting with empty map", e);
}
Map<String, PDComplexFileSpecification> finalExistingNames = existingNames;
attachments.forEach(
attachment -> {
// Create attachments specification
PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
fileSpecification.setFile(attachment.getOriginalFilename());
fileSpecification.setFileDescription(
"Embedded attachment: " + attachment.getOriginalFilename());
try {
// Create embedded attachment
PDEmbeddedFile embeddedFile =
new PDEmbeddedFile(
document, new ByteArrayInputStream(attachment.getBytes()));
embeddedFile.setSize((int) attachment.getSize());
embeddedFile.setCreationDate(new java.util.GregorianCalendar());
embeddedFile.setModDate(new java.util.GregorianCalendar());
// Set MIME type if available
String contentType = attachment.getContentType();
if (StringUtils.isNotBlank(contentType)) {
embeddedFile.setSubtype(contentType);
}
// Associate embedded attachment with file specification
fileSpecification.setEmbeddedFile(embeddedFile);
// Add to the existing names map
finalExistingNames.put(attachment.getOriginalFilename(), fileSpecification);
log.info(
"Added embedded attachment: {} ({} bytes)",
attachment.getOriginalFilename(),
attachment.getSize());
} catch (IOException e) {
log.error(
"Failed to create embedded file for attachment: {}",
attachment.getOriginalFilename(),
e);
}
});
// Update the name tree with all names
efTree.setNames(existingNames);
}
}

View File

@ -0,0 +1,17 @@
package stirling.software.SPDF.service;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.springframework.web.multipart.MultipartFile;
public interface PDFAttachmentServiceInterface {
void addAttachment(
PDDocument document,
PDEmbeddedFilesNameTreeNode efTree,
List<MultipartFile> attachments)
throws IOException;
}

View File

@ -513,6 +513,10 @@ home.addImage.title=Add image
home.addImage.desc=Adds a image onto a set location on the PDF home.addImage.desc=Adds a image onto a set location on the PDF
addImage.tags=img,jpg,picture,photo addImage.tags=img,jpg,picture,photo
home.attachments.title=Attachments
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
attachments.tags=embed,attach,file,attachment,attachments
home.watermark.title=Add Watermark home.watermark.title=Add Watermark
home.watermark.desc=Add a custom watermark to your PDF document. home.watermark.desc=Add a custom watermark to your PDF document.
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
@ -1193,6 +1197,16 @@ addImage.everyPage=Every Page?
addImage.upload=Add image addImage.upload=Add image
addImage.submit=Add image addImage.submit=Add image
#attachments
attachments.title=Attachments
attachments.header=Add attachments to PDF
attachments.removeHeader=Remove attachments from PDF
attachments.selectFiles=Select files to attach
attachments.description=Allows you to add attachments to the PDF
attachments.descriptionPlaceholder=Enter a description for the attachments...
attachments.addButton=Add Attachments
attachments.removeDescription=This will remove all embedded files from the PDF.
attachments.removeButton=Remove All Attachments
#merge #merge
merge.title=Merge merge.title=Merge

View File

@ -474,7 +474,7 @@ addImage.tags=img,jpg,picture,photo
home.attachments.title=Attachments home.attachments.title=Attachments
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
attachments.tags=embed,attach,file,attachment attachments.tags=embed,attach,file,attachment,attachments
home.watermark.title=Add Watermark home.watermark.title=Add Watermark
home.watermark.desc=Add a custom watermark to your PDF document. home.watermark.desc=Add a custom watermark to your PDF document.
@ -484,7 +484,6 @@ home.permissions.title=Change Permissions
home.permissions.desc=Change the permissions of your PDF document home.permissions.desc=Change the permissions of your PDF document
permissions.tags=read,write,edit,print permissions.tags=read,write,edit,print
home.removePages.title=Remove home.removePages.title=Remove
home.removePages.desc=Delete unwanted pages from your PDF document. home.removePages.desc=Delete unwanted pages from your PDF document.
removePages.tags=Remove pages,delete pages removePages.tags=Remove pages,delete pages

View File

@ -237,6 +237,9 @@
<div <div
th:replace="~{fragments/navbarEntry :: navbarEntry('unlock-pdf-forms', 'preview_off', 'home.unlockPDFForms.title', 'home.unlockPDFForms.desc', 'unlockPDFForms.tags', 'other')}"> th:replace="~{fragments/navbarEntry :: navbarEntry('unlock-pdf-forms', 'preview_off', 'home.unlockPDFForms.title', 'home.unlockPDFForms.desc', 'unlockPDFForms.tags', 'other')}">
</div> </div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
</div>
</div> </div>
</div> </div>
<div id="groupAdvanced" class="feature-group"> <div id="groupAdvanced" class="feature-group">

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{attachments.title}, header=#{attachments.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon other">attachment</span>
<span class="tool-header-text" th:text="#{attachments.header}"></span>
</div>
<form action="#" th:action="@{/api/v1/misc/add-attachments}" method="post" enctype="multipart/form-data">
<!-- PDF file selector -->
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<!-- Attachment files selector -->
<div th:replace="~{fragments/common :: fileSelector(name='attachments', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='*/*', inputText=#{attachments.selectFiles})}">
</div>
<!-- Submit button -->
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{attachments.addButton}">Add Attachments</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>