Fixed inaccessible attachments, clean up

This commit is contained in:
Dario Ghunney Ware 2025-06-20 13:22:06 +01:00
parent 244cbe36ff
commit f16ddeb583
59 changed files with 657 additions and 655 deletions

View File

@ -27,5 +27,5 @@ dependencies {
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:2.9'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
api 'jakarta.mail:jakarta.mail-api:2.1.3'
api 'com.sun.mail:jakarta.mail:2.0.1'
}

View File

@ -1,5 +1,7 @@
package stirling.software.common.util;
import static stirling.software.common.util.PDFAttachmentUtils.setCatalogViewerPreferences;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -20,10 +22,7 @@ import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
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.PDPage;
@ -43,7 +42,8 @@ import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
import static stirling.software.common.util.PDFAttachmentUtils.setCatalogViewerPreferences;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
@Slf4j
@UtilityClass
@ -197,8 +197,7 @@ public class EmlToPdf {
boolean disableSanitize)
throws IOException, InterruptedException {
stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest =
createHtmlRequest(request);
HTMLToPdfRequest htmlRequest = createHtmlRequest(request);
try {
return FileToPdf.convertHtmlToPdf(
@ -882,33 +881,33 @@ public class EmlToPdf {
Class<?> messageClass = message.getClass();
// Extract headers via reflection
java.lang.reflect.Method getSubject = messageClass.getMethod("getSubject");
Method getSubject = messageClass.getMethod("getSubject");
String subject = (String) getSubject.invoke(message);
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
java.lang.reflect.Method getFrom = messageClass.getMethod("getFrom");
Method getFrom = messageClass.getMethod("getFrom");
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
content.setFrom(
fromAddresses != null && fromAddresses.length > 0
? safeMimeDecode(fromAddresses[0].toString())
: "");
java.lang.reflect.Method getAllRecipients = messageClass.getMethod("getAllRecipients");
Method getAllRecipients = messageClass.getMethod("getAllRecipients");
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
content.setTo(
recipients != null && recipients.length > 0
? safeMimeDecode(recipients[0].toString())
: "");
java.lang.reflect.Method getSentDate = messageClass.getMethod("getSentDate");
Method getSentDate = messageClass.getMethod("getSentDate");
content.setDate((Date) getSentDate.invoke(message));
// Extract content
java.lang.reflect.Method getContent = messageClass.getMethod("getContent");
Method getContent = messageClass.getMethod("getContent");
Object messageContent = getContent.invoke(message);
if (messageContent instanceof String stringContent) {
java.lang.reflect.Method getContentType = messageClass.getMethod("getContentType");
Method getContentType = messageClass.getMethod("getContentType");
String contentType = (String) getContentType.invoke(message);
if (contentType != null && contentType.toLowerCase().contains("text/html")) {
content.setHtmlBody(stringContent);
@ -947,11 +946,10 @@ public class EmlToPdf {
}
Class<?> multipartClass = multipart.getClass();
java.lang.reflect.Method getCount = multipartClass.getMethod("getCount");
Method getCount = multipartClass.getMethod("getCount");
int count = (Integer) getCount.invoke(multipart);
java.lang.reflect.Method getBodyPart =
multipartClass.getMethod("getBodyPart", int.class);
Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class);
for (int i = 0; i < count; i++) {
Object part = getBodyPart.invoke(multipart, i);
@ -972,12 +970,12 @@ public class EmlToPdf {
}
Class<?> partClass = part.getClass();
java.lang.reflect.Method isMimeType = partClass.getMethod("isMimeType", String.class);
java.lang.reflect.Method getContent = partClass.getMethod("getContent");
java.lang.reflect.Method getDisposition = partClass.getMethod("getDisposition");
java.lang.reflect.Method getFileName = partClass.getMethod("getFileName");
java.lang.reflect.Method getContentType = partClass.getMethod("getContentType");
java.lang.reflect.Method getHeader = partClass.getMethod("getHeader", String.class);
Method isMimeType = partClass.getMethod("isMimeType", String.class);
Method getContent = partClass.getMethod("getContent");
Method getDisposition = partClass.getMethod("getDisposition");
Method getFileName = partClass.getMethod("getFileName");
Method getContentType = partClass.getMethod("getContentType");
Method getHeader = partClass.getMethod("getHeader", String.class);
Object disposition = getDisposition.invoke(part);
String filename = (String) getFileName.invoke(part);
@ -1184,7 +1182,7 @@ public class EmlToPdf {
private static byte[] attachFilesToPdf(
byte[] pdfBytes,
List<EmailAttachment> attachments,
stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory)
CustomPDFDocumentFactory pdfDocumentFactory)
throws IOException {
try (PDDocument document = pdfDocumentFactory.load(pdfBytes);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

View File

@ -1,12 +1,13 @@
package stirling.software.common.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PageMode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PDFAttachmentUtils {
@ -18,24 +19,29 @@ public class PDFAttachmentUtils {
COSDictionary catalogDict = catalog.getCOSObject();
// Set PageMode to UseAttachments - this is the standard PDF specification approach
// PageMode values: UseNone, UseOutlines, UseThumbs, FullScreen, UseOC, UseAttachments
// PageMode values: UseNone, UseOutlines, UseThumbs, FullScreen, UseOC,
// UseAttachments
catalog.setPageMode(pageMode);
catalogDict.setName(COSName.PAGE_MODE, pageMode.stringValue());
// Also set viewer preferences for better attachment viewing experience
COSDictionary viewerPrefs = (COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
COSDictionary viewerPrefs =
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
if (viewerPrefs == null) {
viewerPrefs = new COSDictionary();
catalogDict.setItem(COSName.VIEWER_PREFERENCES, viewerPrefs);
}
// Set NonFullScreenPageMode to UseAttachments as fallback for viewers that support it
viewerPrefs.setName(COSName.getPDFName("NonFullScreenPageMode"), pageMode.stringValue());
// Set NonFullScreenPageMode to UseAttachments as fallback for viewers that support
// it
viewerPrefs.setName(
COSName.getPDFName("NonFullScreenPageMode"), pageMode.stringValue());
// Additional viewer preferences that may help with attachment display
viewerPrefs.setBoolean(COSName.getPDFName("DisplayDocTitle"), true);
log.info("Set PDF PageMode to UseAttachments to automatically show attachments pane");
log.info(
"Set PDF PageMode to UseAttachments to automatically show attachments pane");
}
} catch (Exception e) {
// Log error but don't fail the entire operation for viewer preferences

View File

@ -44,8 +44,7 @@ public class WebResponseUtils {
headers.setContentType(mediaType);
headers.setContentLength(bytes.length);
String encodedDocName =
URLEncoder.encode(docName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
headers.setContentDispositionFormData("attachment", encodedDocName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}

View File

@ -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
@ -83,9 +84,9 @@ public class EndpointConfiguration {
}
public void disableGroup(String group) {
Set<String> endpoints = endpointGroups.get(group);
if (endpoints != null) {
for (String endpoint : endpoints) {
Set<String> disabledEndpoints = endpointGroups.get(group);
if (disabledEndpoints != null) {
for (String endpoint : disabledEndpoints) {
disableEndpoint(endpoint);
}
}

View File

@ -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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
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.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;
@SuppressWarnings("DataFlowIssue")
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
@Operation(
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")
public ResponseEntity<byte[]> addAttachments(
@RequestParam("fileInput") MultipartFile pdfFile,
@RequestParam("attachments") List<MultipartFile> attachments)
throws IOException {
PDDocument document =
pdfAttachmentService.addAttachment(
pdfDocumentFactory.load(pdfFile, false), attachments);
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_with_attachments.pdf");
}
}

View File

@ -1,100 +0,0 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
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.service.PDFAttachmentServiceInterface;
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 AttachmentsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final PDFAttachmentServiceInterface pdfAttachmentService;
@SuppressWarnings("DataFlowIssue")
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
@Operation(
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")
public ResponseEntity<byte[]> addAttachments(
@RequestParam("fileInput") MultipartFile pdfFile,
@RequestParam("attachments") List<MultipartFile> attachments)
throws IOException {
// Load the PDF document
PDDocument document = pdfDocumentFactory.load(pdfFile, false);
// Get or create the document catalog
PDDocumentCatalog catalog = document.getDocumentCatalog();
// Create embedded files name tree if it doesn't exist
PDDocumentNameDictionary documentNames = catalog.getNames();
PDEmbeddedFilesNameTreeNode embeddedFilesTree = new PDEmbeddedFilesNameTreeNode();
if (documentNames != null) {
embeddedFilesTree = documentNames.getEmbeddedFiles();
} else {
documentNames = new PDDocumentNameDictionary(catalog);
documentNames.setEmbeddedFiles(embeddedFilesTree);
}
// Add attachments
catalog.setNames(documentNames);
byte[] output =
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
return WebResponseUtils.bytesToWebResponse(
output,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_with_attachments.pdf");
}
@PostMapping(consumes = "multipart/form-data", value = "/remove-attachments")
@Operation(
summary = "Remove attachments from PDF",
description =
"This endpoint removes all embedded files (attachments) from a PDF. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeAttachments(
@RequestParam("fileInput") MultipartFile pdfFile) throws IOException {
// Load the PDF document and document catalog
PDDocument document = pdfDocumentFactory.load(pdfFile);
PDDocumentCatalog catalog = document.getDocumentCatalog();
// Remove embedded files
if (catalog.getNames() != null) {
catalog.getNames().setEmbeddedFiles(null);
}
// Reset PageMode to UseNone (default)
catalog.setPageMode(PageMode.USE_NONE);
// Return the modified PDF
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_attachments_removed.pdf");
}
}

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.service;
import java.io.ByteArrayOutputStream;
import static stirling.software.common.util.PDFAttachmentUtils.setCatalogViewerPreferences;
import java.io.IOException;
import java.util.GregorianCalendar;
import java.util.HashMap;
@ -9,48 +10,53 @@ 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.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.util.PDFAttachmentUtils;
@Slf4j
@Service
public class PDFAttachmentService implements PDFAttachmentServiceInterface {
public class AttachmentService implements AttachmentServiceInterface {
@Override
public byte[] addAttachment(
PDDocument document,
PDEmbeddedFilesNameTreeNode embeddedFilesTree,
List<MultipartFile> attachments)
public PDDocument addAttachment(PDDocument document, List<MultipartFile> attachments)
throws IOException {
PDDocumentCatalog catalog = document.getDocumentCatalog();
PDDocumentNameDictionary documentNames = catalog.getNames();
PDEmbeddedFilesNameTreeNode embeddedFilesTree = new PDEmbeddedFilesNameTreeNode();
if (documentNames != null) {
embeddedFilesTree = documentNames.getEmbeddedFiles();
} else {
documentNames = new PDDocumentNameDictionary(catalog);
documentNames.setEmbeddedFiles(embeddedFilesTree);
}
catalog.setNames(documentNames);
Map<String, PDComplexFileSpecification> existingNames;
try {
existingNames = embeddedFilesTree.getNames();
Map<String, PDComplexFileSpecification> originalNames = embeddedFilesTree.getNames();
if (existingNames == null) {
if (originalNames == null) {
log.debug("No existing embedded files found, creating new names map.");
existingNames = new HashMap<>();
} else {
existingNames = new HashMap<>(originalNames);
log.debug("Embedded files: {}", existingNames.keySet());
}
log.debug("Embedded files: {}", existingNames.keySet());
} catch (IOException e) {
log.error("Could not retrieve existing embedded files", e);
throw e;
}
grantAccessPermissions(document);
final Map<String, PDComplexFileSpecification> existingEmbeddedFiles = existingNames;
attachments.forEach(
attachment -> {
String filename = attachment.getOriginalFilename();
@ -73,12 +79,10 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface {
fileSpecification.setFile(filename);
fileSpecification.setFileUnicode(filename);
fileSpecification.setFileDescription("Embedded attachment: " + filename);
embeddedFile.setFile(fileSpecification);
fileSpecification.setEmbeddedFile(embeddedFile);
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
// Add to the existing files map
existingEmbeddedFiles.put(filename, fileSpecification);
existingNames.put(filename, fileSpecification);
log.info("Added attachment: {} ({} bytes)", filename, attachment.getSize());
} catch (IOException e) {
@ -87,39 +91,8 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface {
});
embeddedFilesTree.setNames(existingNames);
PDFAttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
return output.toByteArray();
}
private void grantAccessPermissions(PDDocument document) {
try {
AccessPermission currentPermissions = document.getCurrentAccessPermission();
currentPermissions.setCanAssembleDocument(true);
currentPermissions.setCanFillInForm(currentPermissions.canFillInForm());
currentPermissions.setCanModify(true);
currentPermissions.setCanPrint(true);
currentPermissions.setCanPrintFaithful(true);
// Ensure these permissions are enabled for embedded file access
currentPermissions.setCanExtractContent(true);
currentPermissions.setCanExtractForAccessibility(true);
currentPermissions.setCanModifyAnnotations(true);
var protectionPolicy = new StandardProtectionPolicy(null, null, currentPermissions);
if (!document.isAllSecurityToBeRemoved()) {
document.setAllSecurityToBeRemoved(true);
}
document.protect(protectionPolicy);
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
} catch (IOException e) {
throw new RuntimeException(e);
}
return document;
}
}

View File

@ -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;
}

View File

@ -1,17 +0,0 @@
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 {
byte[] addAttachment(
PDDocument document,
PDEmbeddedFilesNameTreeNode efTree,
List<MultipartFile> attachments)
throws IOException;
}

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=كل صفحة؟
addImage.upload=إضافة صورة
addImage.submit=إضافة صورة
#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=دمج
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
fileChooser.extractPDF=جاري الاستخراج...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Releases

View File

@ -1594,6 +1594,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=Buraxılışlar

View File

@ -1594,6 +1594,7 @@ fileChooser.dragAndDropPDF=Влачете и пуснете PDF файл
fileChooser.dragAndDropImage=Влачете и пуснете изображение
fileChooser.hoveredDragAndDrop=Влачете и пуснете файл(ове) тук
fileChooser.extractPDF=Извличане...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Версии

View File

@ -1594,6 +1594,7 @@ fileChooser.dragAndDropPDF=PDF ཡིག་ཆ་འཐེན་ནས་འཇ
fileChooser.dragAndDropImage=པར་རིས་ཡིག་ཆ་འཐེན་ནས་འཇོག་པ།
fileChooser.hoveredDragAndDrop=ཡིག་ཆ་འདིར་འཐེན་ནས་འཇོག་པ།
fileChooser.extractPDF=འདོན་རིས་འགྱུར་བའི་སྒྲིག་བཏང་བ།
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=པར་གཞི།

View File

@ -1594,7 +1594,7 @@ fileChooser.dragAndDropPDF=Arrossega i deixa anar un fitxer PDF
fileChooser.dragAndDropImage=Arrossega i deixa anar un fitxer d'imatge
fileChooser.hoveredDragAndDrop=Arrossega i deixa anar fitxer(s) aquí
fileChooser.extractPDF=Extracting...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Llançaments
releases.title=Notes de Llançament

View File

@ -1594,7 +1594,7 @@ fileChooser.dragAndDropPDF=Přetáhnout PDF soubor
fileChooser.dragAndDropImage=Přetáhnout obrázek
fileChooser.hoveredDragAndDrop=Přetáhněte soubor(y) sem
fileChooser.extractPDF=Extrahování...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Vydání
releases.title=Poznámky k vydání

View File

@ -1594,7 +1594,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
releases.title=Release Notes

View File

@ -1594,6 +1594,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF-Datei
fileChooser.dragAndDropImage=Drag & Drop Bilddatei
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
fileChooser.extractPDF=Extrahiere...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Veröffentlichungen

View File

@ -1594,7 +1594,7 @@ fileChooser.dragAndDropPDF=Σύρετε & αφήστε αρχείο PDF
fileChooser.dragAndDropImage=Σύρετε & αφήστε αρχείο εικόνας
fileChooser.hoveredDragAndDrop=Σύρετε & αφήστε αρχείο(α) εδώ
fileChooser.extractPDF=Εξαγωγή...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Εκδόσεις
releases.title=Σημειώσεις έκδοσης

View File

@ -1212,13 +1212,9 @@ addImage.submit=Add image
#attachments
attachments.title=Add Attachments
attachments.header=Add attachments
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.title=Merge
@ -1608,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

View File

@ -1210,15 +1210,11 @@ addImage.submit=Add image
#attachments
attachments.title=Attachments
attachments.title=Add Attachments
attachments.header=Add attachments
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
@ -1609,6 +1605,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

View File

@ -1205,6 +1205,12 @@ addImage.everyPage=¿Todas las páginas?
addImage.upload=Añadir imagen
addImage.submit=Enviar imagen
#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=Unir
@ -1594,6 +1600,7 @@ fileChooser.dragAndDropPDF=Arrastrar & Soltar archivo PDF
fileChooser.dragAndDropImage=Arrastrar & Soltar archivo de Imagen
fileChooser.hoveredDragAndDrop=Arrastrar & Soltar archivos(s) aquí
fileChooser.extractPDF=Extrayendo...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Versiones

View File

@ -1594,6 +1594,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=هر صفحه؟
addImage.upload=افزودن تصویر
addImage.submit=افزودن تصویر
#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=ادغام
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=فایل(های) خود را اینجا بکشید و رها کنید
fileChooser.extractPDF=در حال استخراج...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=نسخه‌ها

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Toutes les pages ?
addImage.upload=Télécharger une image
addImage.submit=Ajouter une 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=Fusionner
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=Glisser & Déposer le(s) fichier(s) ici
fileChooser.extractPDF=Extraction en cours...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Versions

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Gach Leathanach?
addImage.upload=Cuir íomhá leis
addImage.submit=Cuir íomhá leis
#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=Cumaisc
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Tarraing & Scaoil comhad PDF
fileChooser.dragAndDropImage=Tarraing & Scaoil comhad Íomhá
fileChooser.hoveredDragAndDrop=Tarraing agus scaoil comhad(í) anseo
fileChooser.extractPDF=Ag Aistriú...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Eisiúintí

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=हर पृष्ठ?
addImage.upload=छवि जोड़ें
addImage.submit=छवि जोड़ें
#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=मर्ज करें
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=PDF फ़ाइल खींचें और छो
fileChooser.dragAndDropImage=छवि फ़ाइल खींचें और छोड़ें
fileChooser.hoveredDragAndDrop=फ़ाइल(ें) यहाँ खींचें और छोड़ें
fileChooser.extractPDF=निकालना...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=रिलीज़

View File

@ -1206,6 +1206,14 @@ addImage.upload=Dodaj sliku
addImage.submit=Dodaj sliku
#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=Spajanje
merge.header=Spajanje više PDF-ova (2+)
@ -1594,6 +1602,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Minden oldalra?
addImage.upload=Kép hozzáadása
addImage.submit=Kép hozzáadása
#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=Egyesítés
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Húzza ide a PDF fájlt
fileChooser.dragAndDropImage=Húzza ide a képfájlt
fileChooser.hoveredDragAndDrop=Húzza ide a fájl(oka)t
fileChooser.extractPDF=Kinyerés...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Kiadási jegyzék

View File

@ -1206,6 +1206,14 @@ addImage.upload=Tambahkan Gambar
addImage.submit=Tambahkan Gambar
#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=Gabungkan
merge.header=Gabungkan beberapa PDFs (2+)
@ -1594,6 +1602,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Ogni pagina?
addImage.upload=Aggiungi immagine
addImage.submit=Aggiungi immagine
#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=Unisci
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Trascina & rilascia il file PDF
fileChooser.dragAndDropImage=Trascina & rilascia il file immagine
fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui
fileChooser.extractPDF=Estraendo...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Rilasci

View File

@ -1205,6 +1205,12 @@ addImage.everyPage=全ページ?
addImage.upload=画像の追加
addImage.submit=画像の追加
#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=結合
@ -1594,6 +1600,7 @@ fileChooser.dragAndDropPDF=PDFファイルをドラッグドロップ
fileChooser.dragAndDropImage=画像ファイルをドラッグ&ドロップ
fileChooser.hoveredDragAndDrop=ファイルをここにドラッグ&ドロップ
fileChooser.extractPDF=抽出中...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=リリース

View File

@ -1206,6 +1206,14 @@ addImage.upload=이미지 추가
addImage.submit=이미지 추가
#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.header=여러 PDF 병합 (2개 이상)
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=PDF 파일을 드래그 앤 드롭
fileChooser.dragAndDropImage=이미지 파일을 드래그 앤 드롭
fileChooser.hoveredDragAndDrop=여기에 파일을 드래그 앤 드롭하세요
fileChooser.extractPDF=추출 중...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=릴리스

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=എല്ലാ പേജിലും?
addImage.upload=ചിത്രം ചേർക്കുക
addImage.submit=ചിത്രം ചേർക്കുക
#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=ലയിപ്പിക്കുക
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=PDF ഫയൽ വലിച്ചിടുക
fileChooser.dragAndDropImage=ചിത്ര ഫയൽ വലിച്ചിടുക
fileChooser.hoveredDragAndDrop=ഫയൽ(കൾ) ഇവിടെ വലിച്ചിടുക
fileChooser.extractPDF=വേർതിരിച്ചെടുക്കുന്നു...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=റിലീസുകൾ

View File

@ -1206,6 +1206,14 @@ addImage.upload=Afbeelding toevoegen
addImage.submit=Afbeelding toevoegen
#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=Samenvoegen
merge.header=Meerdere PDF's samenvoegen (2+)
@ -1594,6 +1602,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=På hver side?
addImage.upload=Legg til bilde
addImage.submit=Legg til bilde
#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=Slå sammen
@ -1594,6 +1601,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=Versjoner

View File

@ -1206,6 +1206,14 @@ addImage.upload=Dodaj obraz
addImage.submit=Dodaj obraz
#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=Połącz
merge.header=Połącz wiele dokumentów PDF (2+)
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=Przeciągnij i upuść plik PDF
fileChooser.dragAndDropImage=Przeciągnij i upuść plik obrazu
fileChooser.hoveredDragAndDrop=Przeciągnij i upuść plik(i) tutaj
fileChooser.extractPDF=Trwa wyodrębnianie...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Wydania

View File

@ -1206,6 +1206,14 @@ addImage.upload=Carregar imagem
addImage.submit=Adicionar imagem
#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=Mesclar
merge.header=Mesclar
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=Arraste & Solte PDF(s)
fileChooser.dragAndDropImage=Arraste & Solte Imagem(ns)
fileChooser.hoveredDragAndDrop=Arraste & Solte arquivo(s) aqui
fileChooser.extractPDF=Extraindo...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Versões

View File

@ -1206,6 +1206,14 @@ addImage.upload=Adicionar imagem
addImage.submit=Adicionar imagem
#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=Juntar
merge.header=Juntar múltiplos PDFs (2+)
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=Arrastar e Largar ficheiro PDF
fileChooser.dragAndDropImage=Arrastar e Largar ficheiro de Imagem
fileChooser.hoveredDragAndDrop=Arrastar e Largar ficheiro(s) aqui
fileChooser.extractPDF=Extraindo...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Lançamentos

View File

@ -1206,6 +1206,14 @@ addImage.upload=Adăugare imagine
addImage.submit=Adăugare imagine
#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=Unire
merge.header=Unirea mai multor PDF-uri (2+)
@ -1594,6 +1602,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Каждая страница?
addImage.upload=Добавить изображение
addImage.submit=Добавить изображение
#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=Объединить
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Перетащите PDF-файл
fileChooser.dragAndDropImage=Перетащите файл изображения
fileChooser.hoveredDragAndDrop=Перетащите файл(ы) сюда
fileChooser.extractPDF=Извлечение...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Релизы

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Každá stránka?
addImage.upload=Pridať obrázok
addImage.submit=Pridať obrázok
#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=Zlúčiť
@ -1594,6 +1601,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

View File

@ -1206,6 +1206,14 @@ addImage.upload=Dodaj sliko
addImage.submit=Dodaj sliko
#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=Združi
merge.header=Združi več PDF-jev (2+)
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=Povleci in spusti datoteko PDF
fileChooser.dragAndDropImage=Povleci in spusti slikovno datoteko
fileChooser.hoveredDragAndDrop=Povleci in spusti datoteko(e) sem
fileChooser.extractPDF=Izvlečenje...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Izdaje

View File

@ -1206,6 +1206,14 @@ addImage.upload=Dodaj sliku
addImage.submit=Dodaj sliku
#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=Spajanje
merge.header=Spajanje više PDF fajlova (2+)
@ -1594,6 +1602,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Varje sida?
addImage.upload=Lägg till bild
addImage.submit=Lägg till bild
#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=Sammanfoga
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=Dra & Släpp PDF fil
fileChooser.dragAndDropImage=Dra & Släpp bildfil
fileChooser.hoveredDragAndDrop=Dra & Släpp fil(er) här
fileChooser.extractPDF=Extraherar...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Utgåvor

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=ทุกหน้า?
addImage.upload=เพิ่มรูปภาพ
addImage.submit=เพิ่มรูปภาพ
#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=รวม
@ -1594,6 +1601,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

View File

@ -1205,6 +1205,13 @@ addImage.everyPage=Her Sayfa mı?
addImage.upload=Resim ekle
addImage.submit=Resim ekle
#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=Birleştir
@ -1594,6 +1601,7 @@ fileChooser.dragAndDropPDF=PDF dosyasını Sürükle & Bırak
fileChooser.dragAndDropImage=Görsel dosyasını Sürükle & Bırak
fileChooser.hoveredDragAndDrop=Dosya(lar)ı buraya sürükleyip bırakın
fileChooser.extractPDF=PDF Çıkarılıyor...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Sürümler

View File

@ -1206,6 +1206,14 @@ addImage.upload=Додати зображення
addImage.submit=Додати зображення
#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.header=Об'єднання кількох PDF-файлів (2+)
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=Перетащите PDF-файл
fileChooser.dragAndDropImage=Перетащите файл зображення
fileChooser.hoveredDragAndDrop=Перетащите файл(и) сюда
fileChooser.extractPDF=Видобування...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=Релізи

View File

@ -1206,6 +1206,14 @@ addImage.upload=Thêm hình ảnh
addImage.submit=Thêm hình ảnh
#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=Trộn
merge.header=Trộn nhiều PDF (2+)
@ -1594,6 +1602,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

View File

@ -1206,6 +1206,14 @@ addImage.upload=添加图片
addImage.submit=添加图片
#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.header=合并多个 PDF2个以上
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=拖放PDF文件
fileChooser.dragAndDropImage=拖放图片文件
fileChooser.hoveredDragAndDrop=拖放文件到此处
fileChooser.extractPDF=处理中...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=版本

View File

@ -1206,6 +1206,14 @@ addImage.upload=新增圖片
addImage.submit=新增圖片
#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.header=合併多個 PDF
@ -1594,6 +1602,7 @@ fileChooser.dragAndDropPDF=拖放 PDF 檔案
fileChooser.dragAndDropImage=拖放圖片檔案
fileChooser.hoveredDragAndDrop=將檔案拖放至此
fileChooser.extractPDF=處理中...
fileChooser.addAttachments=drag & drop attachments here
#release notes
releases.footer=版本資訊

View File

@ -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>
`;
}
}

View File

@ -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;

View File

@ -268,6 +268,7 @@
window.fileInput = {
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
addAttachments: '[[#{fileChooser.addAttachments}]]',
extractPDF: '[[#{fileChooser.extractPDF}]]',
loading: '[[#{loading}]]'
};</script>

View File

@ -105,6 +105,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('eml-to-pdf', 'email', 'home.EMLToPDF.title', 'home.EMLToPDF.desc', 'EMLToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
</div>

View File

@ -0,0 +1,123 @@
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.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 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());
mockDocument = mock(PDDocument.class);
modifiedMockDocument = mock(PDDocument.class);
}
@Test
void addAttachments_Success() throws IOException {
List<MultipartFile> attachments = List.of(attachment1, attachment2);
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(pdfFile, attachments);
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);
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(pdfFile, attachments);
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);
IOException ioException = new IOException("Failed to load PDF");
when(pdfDocumentFactory.load(pdfFile, false)).thenThrow(ioException);
assertThrows(IOException.class, () -> attachmentController.addAttachments(pdfFile, attachments));
verify(pdfDocumentFactory).load(pdfFile, false);
verifyNoInteractions(pdfAttachmentService);
}
@Test
void addAttachments_IOExceptionFromAttachmentService() throws IOException {
List<MultipartFile> attachments = List.of(attachment1);
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(pdfFile, attachments));
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
}
}

View File

@ -1,221 +0,0 @@
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 java.io.IOException;
import java.util.List;
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.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.service.PDFAttachmentServiceInterface;
import stirling.software.common.service.CustomPDFDocumentFactory;
@ExtendWith(MockitoExtension.class)
class AttachmentsControllerTest {
@Mock
private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock
private PDFAttachmentServiceInterface pdfAttachmentService;
@InjectMocks
private AttachmentsController attachmentsController;
private MockMultipartFile pdfFile;
private MockMultipartFile attachment1;
private MockMultipartFile attachment2;
private PDDocument mockDocument;
private PDDocumentCatalog mockCatalog;
private PDDocumentNameDictionary mockNameDict;
private PDEmbeddedFilesNameTreeNode mockEmbeddedFilesTree;
@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());
mockDocument = mock(PDDocument.class);
mockCatalog = mock(PDDocumentCatalog.class);
mockNameDict = mock(PDDocumentNameDictionary.class);
mockEmbeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
}
@Test
void addAttachments_WithExistingNames() throws IOException {
List<MultipartFile> attachments = List.of(attachment1, attachment2);
byte[] expectedOutput = "modified PDF content".getBytes();
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, attachments)).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfDocumentFactory).load(pdfFile, false);
verify(mockCatalog).setNames(mockNameDict);
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments);
}
@Test
void addAttachments_WithoutExistingNames() throws IOException {
List<MultipartFile> attachments = List.of(attachment1);
byte[] expectedOutput = "modified PDF content".getBytes();
try (PDDocument realDocument = new PDDocument()) {
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(realDocument);
when(pdfAttachmentService.addAttachment(eq(realDocument), any(PDEmbeddedFilesNameTreeNode.class), eq(attachments))).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfDocumentFactory).load(pdfFile, false);
verify(pdfAttachmentService).addAttachment(eq(realDocument), any(PDEmbeddedFilesNameTreeNode.class), eq(attachments));
}
}
@Test
void addAttachments_IOExceptionFromPDFLoad() throws IOException {
List<MultipartFile> attachments = List.of(attachment1);
IOException ioException = new IOException("Failed to load PDF");
when(pdfDocumentFactory.load(pdfFile, false)).thenThrow(ioException);
assertThrows(IOException.class, () -> attachmentsController.addAttachments(pdfFile, attachments));
verify(pdfDocumentFactory).load(pdfFile, false);
verifyNoInteractions(pdfAttachmentService);
}
@Test
void addAttachments_IOExceptionFromAttachmentService() throws IOException {
List<MultipartFile> attachments = List.of(attachment1);
IOException ioException = new IOException("Failed to add attachment");
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, attachments)).thenThrow(ioException);
assertThrows(IOException.class, () -> attachmentsController.addAttachments(pdfFile, attachments));
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments);
}
@Test
void removeAttachments_WithExistingNames() throws IOException {
when(pdfDocumentFactory.load(pdfFile)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict);
ResponseEntity<byte[]> response = attachmentsController.removeAttachments(pdfFile);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(pdfDocumentFactory).load(pdfFile);
verify(mockNameDict).setEmbeddedFiles(null);
verify(mockCatalog).setPageMode(PageMode.USE_NONE);
}
@Test
void removeAttachments_WithoutExistingNames() throws IOException {
when(pdfDocumentFactory.load(pdfFile)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(null);
ResponseEntity<byte[]> response = attachmentsController.removeAttachments(pdfFile);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(pdfDocumentFactory).load(pdfFile);
verify(mockCatalog).setPageMode(PageMode.USE_NONE);
verifyNoInteractions(mockNameDict);
}
@Test
void removeAttachments_IOExceptionFromPDFLoad() throws IOException {
IOException ioException = new IOException("Failed to load PDF");
when(pdfDocumentFactory.load(pdfFile)).thenThrow(ioException);
assertThrows(IOException.class, () -> attachmentsController.removeAttachments(pdfFile));
verify(pdfDocumentFactory).load(pdfFile);
}
@Test
void addAttachments_EmptyAttachmentsList() throws IOException {
List<MultipartFile> emptyAttachments = List.of();
byte[] expectedOutput = "PDF content without new attachments".getBytes();
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, emptyAttachments)).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, emptyAttachments);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, emptyAttachments);
}
@Test
void addAttachments_NullFilename() throws IOException {
MockMultipartFile attachmentWithNullName = new MockMultipartFile("attachment", null, "text/plain", "content".getBytes());
List<MultipartFile> attachments = List.of(attachmentWithNullName);
byte[] expectedOutput = "PDF with null filename attachment".getBytes();
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, attachments)).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments);
}
@Test
void removeAttachments_NullPDFFilename() throws IOException {
MockMultipartFile pdfWithNullName = new MockMultipartFile("fileInput", null, "application/pdf", "PDF content".getBytes());
when(pdfDocumentFactory.load(pdfWithNullName)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(null);
ResponseEntity<byte[]> response = attachmentsController.removeAttachments(pdfWithNullName);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(mockCatalog).setPageMode(PageMode.USE_NONE);
}
}

View File

@ -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());
}
}
}

View File

@ -1,216 +0,0 @@
package stirling.software.SPDF.service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
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.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class PDFAttachmentServiceTest {
private PDFAttachmentService pdfAttachmentService;
@BeforeEach
void setUp() {
pdfAttachmentService = new PDFAttachmentService();
}
@Test
void addAttachmentToPDF() throws IOException {
try (var document = new PDDocument()) {
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
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");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithNullExistingNames() throws IOException {
try (var document = new PDDocument()) {
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
when(embeddedFilesTree.getNames()).thenReturn(null);
when(attachments.get(0).getOriginalFilename()).thenReturn("document.pdf");
when(attachments.get(0).getInputStream()).thenReturn(
new ByteArrayInputStream("PDF content".getBytes()));
when(attachments.get(0).getSize()).thenReturn(15L);
when(attachments.get(0).getContentType()).thenReturn("application/pdf");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithBlankContentType() throws IOException {
try (var document = new PDDocument()) {
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
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("");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_GetNamesThrowsIOException() throws IOException {
var document = mock(PDDocument.class);
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var ioException = new IOException("Failed to retrieve embedded files");
when(embeddedFilesTree.getNames()).thenThrow(ioException);
assertThrows(IOException.class, () -> pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments));
verify(embeddedFilesTree).getNames();
}
@Test
void addAttachmentToPDF_AttachmentInputStreamThrowsIOException() throws IOException {
try (var document = new PDDocument()) {
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
var ioException = new IOException("Failed to read attachment stream");
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
when(attachments.get(0).getOriginalFilename()).thenReturn("corrupted.file");
when(attachments.get(0).getInputStream()).thenThrow(ioException);
when(attachments.get(0).getSize()).thenReturn(10L);
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithProtectedDocument() throws IOException {
try (var document = new PDDocument()) {
// Create a document with restricted permissions (this simulates an encrypted/protected document)
AccessPermission ap = new AccessPermission();
ap.setCanExtractContent(false); // Restrict content extraction initially
var spp = new StandardProtectionPolicy("owner", "user", ap);
document.protect(spp);
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
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");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithRestrictedPermissions() throws IOException {
try (var document = new PDDocument()) {
// Create a document with very restricted permissions that should block permission changes
AccessPermission ap = new AccessPermission();
ap.setCanModify(false);
ap.setCanAssembleDocument(false);
ap.setCanExtractContent(false);
var spp = new StandardProtectionPolicy("owner", "user", ap);
document.protect(spp);
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
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");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithNonEncryptedDocument() throws IOException {
try (var document = new PDDocument()) {
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
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");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
// Verify permissions are set correctly for non-encrypted documents
AccessPermission permissions = document.getCurrentAccessPermission();
assertTrue(permissions.canExtractContent());
assertTrue(permissions.canExtractForAccessibility());
assertTrue(permissions.canModifyAnnotations());
verify(embeddedFilesTree).setNames(anyMap());
}
}
}