mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Add Attachments Feature (#3781)
# Description of Changes Added a new feature to add attachments to a PDF document. ### Added: - `AttachmentController`: Endpoint for adding attachments at `/add-attachments` with parameters `fileInput` for the PDF and `attachments` as a list of files to attach - `AttachmentServiceInterface` - `AttachmentService`: Handles the logic of adding attachments to the PDF - `AttachmentUtils`: to handle setting the catalog viewer preferences in the viewer - Add Attachments page - Tests for new feature ### Changes: - `EmlToPdf`: Moved setting of viewer preferences to `AttachmentUtils` - `EndpointConfiguration: Included '/add-attachments' - Updated language files with attachments copy - General clean up Closes #1259 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [x] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [x] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable)     - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Service
|
||||
@@ -173,6 +174,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||
addEndpointToGroup("Other", "show-javascript");
|
||||
addEndpointToGroup("Other", "remove-image-pdf");
|
||||
addEndpointToGroup("Other", "add-attachments");
|
||||
|
||||
// CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
@@ -251,6 +253,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "pdf-to-text");
|
||||
addEndpointToGroup("Java", "remove-image-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||
addEndpointToGroup("Java", "add-attachments");
|
||||
|
||||
// Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
|
||||
+1
-1
@@ -225,7 +225,7 @@ public class MergeController {
|
||||
String mergedFileName =
|
||||
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_merged_unsigned.pdf";
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
return WebResponseUtils.baosToWebResponse(
|
||||
baos, mergedFileName); // Return the modified PDF
|
||||
|
||||
} catch (Exception ex) {
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
|
||||
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class AttachmentController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
private final AttachmentServiceInterface pdfAttachmentService;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
|
||||
@Operation(
|
||||
summary = "Add attachments to PDF",
|
||||
description =
|
||||
"This endpoint adds attachments to a PDF. Input:PDF, Output:PDF Type:MISO")
|
||||
public ResponseEntity<byte[]> addAttachments(@ModelAttribute AddAttachmentRequest request)
|
||||
throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
List<MultipartFile> attachments = request.getAttachments();
|
||||
|
||||
PDDocument document =
|
||||
pdfAttachmentService.addAttachment(
|
||||
pdfDocumentFactory.load(fileInput, false), attachments);
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_with_attachments.pdf");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -144,7 +144,7 @@ public class BlankPageController {
|
||||
zos.close();
|
||||
|
||||
log.info("Returning ZIP file: {}", filename + "_processed.zip");
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
return WebResponseUtils.baosToWebResponse(
|
||||
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
} catch (IOException e) {
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ public class ExtractImagesController {
|
||||
// Create ByteArrayResource from byte array
|
||||
byte[] zipContents = baos.toByteArray();
|
||||
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
return WebResponseUtils.baosToWebResponse(
|
||||
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ public class PipelineController {
|
||||
}
|
||||
zipOut.close();
|
||||
log.info("Returning zipped file response...");
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
return WebResponseUtils.baosToWebResponse(
|
||||
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
} catch (Exception e) {
|
||||
log.error("Error handling data: ", e);
|
||||
|
||||
+1
-1
@@ -205,7 +205,7 @@ public class CertSignController {
|
||||
location,
|
||||
reason,
|
||||
showLogo);
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
return WebResponseUtils.baosToWebResponse(
|
||||
baos,
|
||||
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||
+ "_signed.pdf");
|
||||
|
||||
+7
@@ -191,4 +191,11 @@ public class OtherWebController {
|
||||
model.addAttribute("currentPage", "auto-rename");
|
||||
return "misc/auto-rename";
|
||||
}
|
||||
|
||||
@GetMapping("/add-attachments")
|
||||
@Hidden
|
||||
public String attachmentsForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-attachments");
|
||||
return "misc/add-attachments";
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package stirling.software.SPDF.model.api.misc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AddAttachmentRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "The image file to be overlaid onto the PDF.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
format = "binary")
|
||||
private List<MultipartFile> attachments;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
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.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||
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.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AttachmentService implements AttachmentServiceInterface {
|
||||
|
||||
@Override
|
||||
public PDDocument addAttachment(PDDocument document, List<MultipartFile> attachments)
|
||||
throws IOException {
|
||||
PDEmbeddedFilesNameTreeNode embeddedFilesTree = getEmbeddedFilesTree(document);
|
||||
Map<String, PDComplexFileSpecification> existingNames;
|
||||
|
||||
try {
|
||||
Map<String, PDComplexFileSpecification> names = embeddedFilesTree.getNames();
|
||||
|
||||
if (names == null) {
|
||||
log.debug("No existing embedded files found, creating new names map.");
|
||||
existingNames = new HashMap<>();
|
||||
} else {
|
||||
existingNames = new HashMap<>(names);
|
||||
log.debug("Embedded files: {}", existingNames.keySet());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Could not retrieve existing embedded files", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
attachments.forEach(
|
||||
attachment -> {
|
||||
String filename = attachment.getOriginalFilename();
|
||||
|
||||
try {
|
||||
PDEmbeddedFile embeddedFile =
|
||||
new PDEmbeddedFile(document, attachment.getInputStream());
|
||||
embeddedFile.setSize((int) attachment.getSize());
|
||||
embeddedFile.setCreationDate(new GregorianCalendar());
|
||||
embeddedFile.setModDate(new GregorianCalendar());
|
||||
String contentType = attachment.getContentType();
|
||||
if (StringUtils.isNotBlank(contentType)) {
|
||||
embeddedFile.setSubtype(contentType);
|
||||
}
|
||||
|
||||
// Create attachments specification and associate embedded attachment with
|
||||
// file
|
||||
PDComplexFileSpecification fileSpecification =
|
||||
new PDComplexFileSpecification();
|
||||
fileSpecification.setFile(filename);
|
||||
fileSpecification.setFileUnicode(filename);
|
||||
fileSpecification.setFileDescription("Embedded attachment: " + filename);
|
||||
fileSpecification.setEmbeddedFile(embeddedFile);
|
||||
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
|
||||
|
||||
existingNames.put(filename, fileSpecification);
|
||||
|
||||
log.info("Added attachment: {} ({} bytes)", filename, attachment.getSize());
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to create embedded file for attachment: {}", filename, e);
|
||||
}
|
||||
});
|
||||
|
||||
embeddedFilesTree.setNames(existingNames);
|
||||
setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private PDEmbeddedFilesNameTreeNode getEmbeddedFilesTree(PDDocument document) {
|
||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||
PDDocumentNameDictionary documentNames = catalog.getNames();
|
||||
|
||||
if (documentNames == null) {
|
||||
documentNames = new PDDocumentNameDictionary(catalog);
|
||||
}
|
||||
|
||||
catalog.setNames(documentNames);
|
||||
PDEmbeddedFilesNameTreeNode embeddedFilesTree = documentNames.getEmbeddedFiles();
|
||||
|
||||
if (embeddedFilesTree == null) {
|
||||
embeddedFilesTree = new PDEmbeddedFilesNameTreeNode();
|
||||
documentNames.setEmbeddedFiles(embeddedFilesTree);
|
||||
}
|
||||
return embeddedFilesTree;
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface AttachmentServiceInterface {
|
||||
|
||||
PDDocument addAttachment(PDDocument document, List<MultipartFile> attachments)
|
||||
throws IOException;
|
||||
}
|
||||
@@ -525,6 +525,10 @@ home.addImage.title=Add image
|
||||
home.addImage.desc=Adds a image onto a set location on the PDF
|
||||
addImage.tags=img,jpg,picture,photo
|
||||
|
||||
home.attachments.title=Add 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.desc=Add a custom watermark to your PDF document.
|
||||
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
||||
@@ -1205,6 +1209,12 @@ addImage.everyPage=Every Page?
|
||||
addImage.upload=Add image
|
||||
addImage.submit=Add image
|
||||
|
||||
#attachments
|
||||
attachments.title=Add Attachments
|
||||
attachments.header=Add attachments
|
||||
attachments.description=Allows you to add attachments to the PDF
|
||||
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||
attachments.addButton=Add Attachments
|
||||
|
||||
#merge
|
||||
merge.title=Merge
|
||||
@@ -1594,6 +1604,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
fileChooser.extractPDF=Extracting...
|
||||
fileChooser.addAttachments=drag & drop attachments here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
|
||||
@@ -6,6 +6,9 @@ class FileIconFactory {
|
||||
return this.createPDFIcon();
|
||||
case "csv":
|
||||
return this.createCSVIcon();
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
return this.createXLSXIcon();
|
||||
case "jpe":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
@@ -44,8 +47,29 @@ class FileIconFactory {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M216-144q-30 0-51-21.5T144-216v-528q0-29 21-50.5t51-21.5h528q30 0 51 21.5t21 50.5v528q0 29-21 50.5T744-144H216Zm48-144h432L552-480 444-336l-72-96-108 144Z"/></svg>`;
|
||||
}
|
||||
|
||||
static createCSVIcon() {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-filetype-csv" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM3.517 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.54.185-.382.185-.816 0-.335-.123-.628a1.4 1.4 0 0 0-.366-.486 1.8 1.8 0 0 0-.614-.314 2.8 2.8 0 0 0-.865-.118 2.1 2.1 0 0 0-.614.094 1.4 1.4 0 0 0-.471.264 1.1 1.1 0 0 0-.298.429.9.9 0 0 0-.103.539h.606a.4.4 0 0 1 .096-.258.5.5 0 0 1 .213-.164.6.6 0 0 1 .33-.082.7.7 0 0 1 .458.132.4.4 0 0 1 .153.372.4.4 0 0 1-.085.235.7.7 0 0 1-.25.192 1.4 1.4 0 0 1-.407.115c-.127.023-.266.05-.416.081a1.8 1.8 0 0 0-.534.187 1.2 1.2 0 0 0-.382.346 1 1 0 0 0-.138.537q0 .295.101.517M8.717 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.54.185-.382.185-.816 0-.335-.123-.628a1.4 1.4 0 0 0-.366-.486 1.8 1.8 0 0 0-.614-.314 2.8 2.8 0 0 0-.865-.118 2.1 2.1 0 0 0-.614.094 1.4 1.4 0 0 0-.471.264 1.1 1.1 0 0 0-.298.429.9.9 0 0 0-.103.539h.606a.4.4 0 0 1 .096-.258.5.5 0 0 1 .213-.164.6.6 0 0 1 .33-.082.7.7 0 0 1 .458.132.4.4 0 0 1 .153.372.4.4 0 0 1-.085.235.7.7 0 0 1-.25.192 1.4 1.4 0 0 1-.407.115c-.127.023-.266.05-.416.081a1.8 1.8 0 0 0-.534.187 1.2 1.2 0 0 0-.382.346 1 1 0 0 0-.138.537q0 .295.101.517M14.229 13.12v.506H11.85v-.506h1.063v-1.277H11.85v-.506h2.379v.506h-1.063z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static createXLSXIcon() {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark-excel" viewBox="0 0 16 16">
|
||||
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219l-2.116-2.54z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static createUnknownFileIcon() {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M263.72-96Q234-96 213-117.15T192-168v-624q0-29.7 21.15-50.85Q234.3-864 264-864h312l192 192v504q0 29.7-21.16 50.85Q725.68-96 695.96-96H263.72ZM528-624h168L528-792v168Z"/></svg>`;
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16">
|
||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ function setupFileInput(chooser) {
|
||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropPDF;
|
||||
} else if (inputContainer.id === 'image-upload-input-container') {
|
||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropImage;
|
||||
} else if (inputContainer.id === 'attachments-input-container') {
|
||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.addAttachments;
|
||||
}
|
||||
let allFiles = [];
|
||||
let overlay;
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
window.fileInput = {
|
||||
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
|
||||
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
|
||||
addAttachments: '[[#{fileChooser.addAttachments}]]',
|
||||
extractPDF: '[[#{fileChooser.extractPDF}]]',
|
||||
loading: '[[#{loading}]]'
|
||||
};</script>
|
||||
|
||||
@@ -236,7 +236,10 @@
|
||||
</div>
|
||||
<div
|
||||
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('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupAdvanced" class="feature-group">
|
||||
|
||||
@@ -290,6 +290,9 @@
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='unlock-pdf-forms', cardTitle=#{home.unlockPDFForms.title}, cardText=#{home.unlockPDFForms.desc}, cardLink='unlock-pdf-forms', toolIcon='preview_off', tags=#{unlockPDFForms.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
|
||||
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AttachmentControllerTest {
|
||||
|
||||
@Mock
|
||||
private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Mock
|
||||
private AttachmentServiceInterface pdfAttachmentService;
|
||||
|
||||
@InjectMocks
|
||||
private AttachmentController attachmentController;
|
||||
|
||||
private MockMultipartFile pdfFile;
|
||||
private MockMultipartFile attachment1;
|
||||
private MockMultipartFile attachment2;
|
||||
private AddAttachmentRequest request;
|
||||
private PDDocument mockDocument;
|
||||
private PDDocument modifiedMockDocument;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
pdfFile = new MockMultipartFile("fileInput", "test.pdf", "application/pdf", "PDF content".getBytes());
|
||||
attachment1 = new MockMultipartFile("attachment1", "file1.txt", "text/plain", "File 1 content".getBytes());
|
||||
attachment2 = new MockMultipartFile("attachment2", "file2.jpg", "image/jpeg", "Image content".getBytes());
|
||||
request = new AddAttachmentRequest();
|
||||
mockDocument = mock(PDDocument.class);
|
||||
modifiedMockDocument = mock(PDDocument.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachments_Success() throws IOException {
|
||||
List<MultipartFile> attachments = List.of(attachment1, attachment2);
|
||||
request.setAttachments(attachments);
|
||||
request.setFileInput(pdfFile);
|
||||
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("modified PDF content".getBytes());
|
||||
|
||||
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
|
||||
when(pdfAttachmentService.addAttachment(mockDocument, attachments)).thenReturn(modifiedMockDocument);
|
||||
|
||||
try (MockedStatic<WebResponseUtils> mockedWebResponseUtils = mockStatic(WebResponseUtils.class)) {
|
||||
mockedWebResponseUtils.when(() -> WebResponseUtils.pdfDocToWebResponse(eq(modifiedMockDocument), eq("test_with_attachments.pdf")))
|
||||
.thenReturn(expectedResponse);
|
||||
|
||||
ResponseEntity<byte[]> response = attachmentController.addAttachments(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
verify(pdfDocumentFactory).load(pdfFile, false);
|
||||
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachments_SingleAttachment() throws IOException {
|
||||
List<MultipartFile> attachments = List.of(attachment1);
|
||||
request.setAttachments(attachments);
|
||||
request.setFileInput(pdfFile);
|
||||
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("modified PDF content".getBytes());
|
||||
|
||||
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
|
||||
when(pdfAttachmentService.addAttachment(mockDocument, attachments)).thenReturn(modifiedMockDocument);
|
||||
|
||||
try (MockedStatic<WebResponseUtils> mockedWebResponseUtils = mockStatic(WebResponseUtils.class)) {
|
||||
mockedWebResponseUtils.when(() -> WebResponseUtils.pdfDocToWebResponse(eq(modifiedMockDocument), eq("test_with_attachments.pdf")))
|
||||
.thenReturn(expectedResponse);
|
||||
|
||||
ResponseEntity<byte[]> response = attachmentController.addAttachments(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
verify(pdfDocumentFactory).load(pdfFile, false);
|
||||
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachments_IOExceptionFromPDFLoad() throws IOException {
|
||||
List<MultipartFile> attachments = List.of(attachment1);
|
||||
request.setAttachments(attachments);
|
||||
request.setFileInput(pdfFile);
|
||||
IOException ioException = new IOException("Failed to load PDF");
|
||||
|
||||
when(pdfDocumentFactory.load(pdfFile, false)).thenThrow(ioException);
|
||||
|
||||
assertThrows(IOException.class, () -> attachmentController.addAttachments(request));
|
||||
verify(pdfDocumentFactory).load(pdfFile, false);
|
||||
verifyNoInteractions(pdfAttachmentService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachments_IOExceptionFromAttachmentService() throws IOException {
|
||||
List<MultipartFile> attachments = List.of(attachment1);
|
||||
request.setAttachments(attachments);
|
||||
request.setFileInput(pdfFile);
|
||||
IOException ioException = new IOException("Failed to add attachment");
|
||||
|
||||
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
|
||||
when(pdfAttachmentService.addAttachment(mockDocument, attachments)).thenThrow(ioException);
|
||||
|
||||
assertThrows(IOException.class, () -> attachmentController.addAttachments(request));
|
||||
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AttachmentServiceTest {
|
||||
|
||||
private AttachmentService attachmentService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
attachmentService = new AttachmentService();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachmentToPDF() throws IOException {
|
||||
try (var document = new PDDocument()) {
|
||||
document.setDocumentId(100L);
|
||||
var attachments = List.of(mock(MultipartFile.class));
|
||||
|
||||
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
|
||||
when(attachments.get(0).getInputStream()).thenReturn(
|
||||
new ByteArrayInputStream("Test content".getBytes()));
|
||||
when(attachments.get(0).getSize()).thenReturn(12L);
|
||||
when(attachments.get(0).getContentType()).thenReturn("text/plain");
|
||||
|
||||
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(document.getDocumentId(), result.getDocumentId());
|
||||
assertNotNull(result.getDocumentCatalog().getNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachmentToPDF_MultipleAttachments() throws IOException {
|
||||
try (var document = new PDDocument()) {
|
||||
document.setDocumentId(100L);
|
||||
var attachment1 = mock(MultipartFile.class);
|
||||
var attachment2 = mock(MultipartFile.class);
|
||||
var attachments = List.of(attachment1, attachment2);
|
||||
|
||||
when(attachment1.getOriginalFilename()).thenReturn("document.pdf");
|
||||
when(attachment1.getInputStream()).thenReturn(
|
||||
new ByteArrayInputStream("PDF content".getBytes()));
|
||||
when(attachment1.getSize()).thenReturn(15L);
|
||||
when(attachment1.getContentType()).thenReturn("application/pdf");
|
||||
|
||||
when(attachment2.getOriginalFilename()).thenReturn("image.jpg");
|
||||
when(attachment2.getInputStream()).thenReturn(
|
||||
new ByteArrayInputStream("Image content".getBytes()));
|
||||
when(attachment2.getSize()).thenReturn(20L);
|
||||
when(attachment2.getContentType()).thenReturn("image/jpeg");
|
||||
|
||||
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getDocumentCatalog().getNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachmentToPDF_WithBlankContentType() throws IOException {
|
||||
try (var document = new PDDocument()) {
|
||||
document.setDocumentId(100L);
|
||||
var attachments = List.of(mock(MultipartFile.class));
|
||||
|
||||
when(attachments.get(0).getOriginalFilename()).thenReturn("image.jpg");
|
||||
when(attachments.get(0).getInputStream()).thenReturn(
|
||||
new ByteArrayInputStream("Image content".getBytes()));
|
||||
when(attachments.get(0).getSize()).thenReturn(25L);
|
||||
when(attachments.get(0).getContentType()).thenReturn("");
|
||||
|
||||
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getDocumentCatalog().getNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAttachmentToPDF_AttachmentInputStreamThrowsIOException() throws IOException {
|
||||
try (var document = new PDDocument()) {
|
||||
var attachments = List.of(mock(MultipartFile.class));
|
||||
var ioException = new IOException("Failed to read attachment stream");
|
||||
|
||||
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
|
||||
when(attachments.get(0).getInputStream()).thenThrow(ioException);
|
||||
when(attachments.get(0).getSize()).thenReturn(10L);
|
||||
|
||||
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getDocumentCatalog().getNames());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user