Compare commits

...

8 Commits

Author SHA1 Message Date
Dario Ghunney Ware
d133997372 wip 2025-06-19 11:39:11 +01:00
Dario Ghunney Ware
5c5f2ecb59 changed response type, updated tests 2025-06-18 15:13:27 +01:00
Dario Ghunney Ware
978e6f3d0c added attachments text to language files 2025-06-18 11:59:31 +01:00
Dario Ghunney Ware
beb1f35abd added tests 2025-06-18 11:38:11 +01:00
Dario Ghunney Ware
f731c00457 fixing access to attachments 2025-06-18 11:38:11 +01:00
Dario Ghunney Ware
c281dbb4bb fixing routing to page 2025-06-18 11:38:02 +01:00
Dario Ghunney Ware
5304381236 created AttachmentsController 2025-06-12 11:40:21 +01:00
Dario Ghunney Ware
b3e8081aa6 setting up AttachmentsController 2025-06-12 11:40:19 +01:00
64 changed files with 1419 additions and 59 deletions

View File

@ -19,14 +19,15 @@ import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import lombok.Data;
import org.apache.pdfbox.cos.COSDictionary; import lombok.Getter;
import org.apache.pdfbox.cos.COSName; import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PageMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
@ -35,12 +36,8 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import lombok.Data;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.model.api.converters.EmlToPdfRequest;
import static stirling.software.common.util.PDFAttachmentUtils.setCatalogViewerPreferences;
@Slf4j @Slf4j
@UtilityClass @UtilityClass
@ -1107,15 +1104,13 @@ public class EmlToPdf {
new PDEmbeddedFile(document, new ByteArrayInputStream(attachment.getData())); new PDEmbeddedFile(document, new ByteArrayInputStream(attachment.getData()));
embeddedFile.setSize(attachment.getData().length); embeddedFile.setSize(attachment.getData().length);
embeddedFile.setCreationDate(new GregorianCalendar()); embeddedFile.setCreationDate(new GregorianCalendar());
if (attachment.getContentType() != null) {
embeddedFile.setSubtype(attachment.getContentType());
}
// Create file specification // Create file specification
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification(); PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
fileSpec.setFile(uniqueFilename); fileSpec.setFile(uniqueFilename);
fileSpec.setEmbeddedFile(embeddedFile); fileSpec.setEmbeddedFile(embeddedFile);
if (attachment.getContentType() != null) { if (attachment.getContentType() != null) {
embeddedFile.setSubtype(attachment.getContentType());
fileSpec.setFileDescription("Email attachment: " + uniqueFilename); fileSpec.setFileDescription("Email attachment: " + uniqueFilename);
} }
@ -1137,7 +1132,7 @@ public class EmlToPdf {
efTree.setNames(efMap); efTree.setNames(efMap);
// Set catalog viewer preferences to automatically show attachments pane // Set catalog viewer preferences to automatically show attachments pane
setCatalogViewerPreferences(document); setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
} }
// Add attachment annotations to the first page for each embedded file // Add attachment annotations to the first page for each embedded file
@ -1285,38 +1280,6 @@ public class EmlToPdf {
} }
} }
private static void setCatalogViewerPreferences(PDDocument document) {
try {
PDDocumentCatalog catalog = document.getDocumentCatalog();
if (catalog != null) {
// Get the catalog's COS dictionary to work with low-level PDF objects
COSDictionary catalogDict = catalog.getCOSObject();
// Set PageMode to UseAttachments - this is the standard PDF specification approach
// PageMode values: UseNone, UseOutlines, UseThumbs, FullScreen, UseOC, UseAttachments
catalogDict.setName(COSName.PAGE_MODE, "UseAttachments");
// Also set viewer preferences for better attachment viewing experience
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"), "UseAttachments");
// 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");
}
} catch (Exception e) {
// Log warning but don't fail the entire operation for viewer preferences
log.warn("Failed to set catalog viewer preferences for attachments", e);
}
}
// MIME header decoding functionality for RFC 2047 encoded headers - moved to constants // MIME header decoding functionality for RFC 2047 encoded headers - moved to constants
private static String decodeMimeHeader(String encodedText) { private static String decodeMimeHeader(String encodedText) {

View File

@ -0,0 +1,45 @@
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;
@Slf4j
public class PDFAttachmentUtils {
public static void setCatalogViewerPreferences(PDDocument document, PageMode pageMode) {
try {
PDDocumentCatalog catalog = document.getDocumentCatalog();
if (catalog != null) {
// Get the catalog's COS dictionary to work with low-level PDF objects
COSDictionary catalogDict = catalog.getCOSObject();
// Set PageMode to UseAttachments - this is the standard PDF specification approach
// 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);
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());
// 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");
}
} catch (Exception e) {
// Log error but don't fail the entire operation for viewer preferences
log.error("Failed to set catalog viewer preferences for attachments", e);
}
}
}

View File

@ -16,12 +16,12 @@ import io.github.pixee.security.Filenames;
public class WebResponseUtils { public class WebResponseUtils {
public static ResponseEntity<byte[]> boasToWebResponse( public static ResponseEntity<byte[]> baosToWebResponse(
ByteArrayOutputStream baos, String docName) throws IOException { ByteArrayOutputStream baos, String docName) throws IOException {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
} }
public static ResponseEntity<byte[]> boasToWebResponse( public static ResponseEntity<byte[]> baosToWebResponse(
ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
} }
@ -44,7 +44,7 @@ public class WebResponseUtils {
headers.setContentType(mediaType); headers.setContentType(mediaType);
headers.setContentLength(bytes.length); headers.setContentLength(bytes.length);
String encodedDocName = String encodedDocName =
URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()) URLEncoder.encode(docName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"); .replaceAll("\\+", "%20");
headers.setContentDispositionFormData("attachment", encodedDocName); headers.setContentDispositionFormData("attachment", encodedDocName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK); return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
@ -61,9 +61,8 @@ public class WebResponseUtils {
// Open Byte Array and save document to it // Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos); document.save(baos);
// Close the document
document.close(); document.close();
return boasToWebResponse(baos, docName); return baosToWebResponse(baos, docName);
} }
} }

View File

@ -25,7 +25,7 @@ public class WebResponseUtilsTest {
String docName = "sample.pdf"; String docName = "sample.pdf";
ResponseEntity<byte[]> responseEntity = ResponseEntity<byte[]> responseEntity =
WebResponseUtils.boasToWebResponse(baos, docName); WebResponseUtils.baosToWebResponse(baos, docName);
assertNotNull(responseEntity); assertNotNull(responseEntity);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); assertEquals(HttpStatus.OK, responseEntity.getStatusCode());

View File

@ -173,6 +173,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("Other", "show-javascript");
addEndpointToGroup("Other", "remove-image-pdf"); addEndpointToGroup("Other", "remove-image-pdf");
addEndpointToGroup("Other", "add-attachments");
// CLI // CLI
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "compress-pdf");
@ -251,6 +252,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "pdf-to-text"); addEndpointToGroup("Java", "pdf-to-text");
addEndpointToGroup("Java", "remove-image-pdf"); addEndpointToGroup("Java", "remove-image-pdf");
addEndpointToGroup("Java", "pdf-to-markdown"); addEndpointToGroup("Java", "pdf-to-markdown");
addEndpointToGroup("Java", "add-attachments");
// Javascript // Javascript
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "pdf-organizer");

View File

@ -225,7 +225,7 @@ public class MergeController {
String mergedFileName = String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf"; + "_merged_unsigned.pdf";
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.baosToWebResponse(
baos, mergedFileName); // Return the modified PDF baos, mergedFileName); // Return the modified PDF
} catch (Exception ex) { } catch (Exception ex) {

View File

@ -0,0 +1,100 @@
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

@ -144,7 +144,7 @@ public class BlankPageController {
zos.close(); zos.close();
log.info("Returning ZIP file: {}", filename + "_processed.zip"); log.info("Returning ZIP file: {}", filename + "_processed.zip");
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.baosToWebResponse(
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM); baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (IOException e) { } catch (IOException e) {

View File

@ -148,7 +148,7 @@ public class ExtractImagesController {
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.baosToWebResponse(
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
} }

View File

@ -118,7 +118,7 @@ public class PipelineController {
} }
zipOut.close(); zipOut.close();
log.info("Returning zipped file response..."); log.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.baosToWebResponse(
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) { } catch (Exception e) {
log.error("Error handling data: ", e); log.error("Error handling data: ", e);

View File

@ -205,7 +205,7 @@ public class CertSignController {
location, location,
reason, reason,
showLogo); showLogo);
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.baosToWebResponse(
baos, baos,
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
+ "_signed.pdf"); + "_signed.pdf");

View File

@ -191,4 +191,11 @@ public class OtherWebController {
model.addAttribute("currentPage", "auto-rename"); model.addAttribute("currentPage", "auto-rename");
return "misc/auto-rename"; return "misc/auto-rename";
} }
@GetMapping("/add-attachments")
@Hidden
public String attachmentsForm(Model model) {
model.addAttribute("currentPage", "add-attachments");
return "misc/add-attachments";
}
} }

View File

@ -0,0 +1,125 @@
package stirling.software.SPDF.service;
import java.io.ByteArrayOutputStream;
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.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 {
@Override
public byte[] addAttachment(
PDDocument document,
PDEmbeddedFilesNameTreeNode embeddedFilesTree,
List<MultipartFile> attachments)
throws IOException {
Map<String, PDComplexFileSpecification> existingNames;
try {
existingNames = embeddedFilesTree.getNames();
if (existingNames == null) {
log.debug("No existing embedded files found, creating new names map.");
existingNames = new HashMap<>();
}
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();
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);
embeddedFile.setFile(fileSpecification);
fileSpecification.setEmbeddedFile(embeddedFile);
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
// Add to the existing files map
existingEmbeddedFiles.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);
PDFAttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
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);
}
}
}

View File

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

View File

@ -471,6 +471,10 @@ home.addImage.title=????? ???? ??? ??? PDF
home.addImage.desc=????? ???? ??? ???? ???? ?? PDF (????? ??? ??????) home.addImage.desc=????? ???? ??? ???? ???? ?? PDF (????? ??? ??????)
addImage.tags=????,jpg,????,???? ?????????? addImage.tags=????,jpg,????,???? ??????????
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=????? ????? ????? home.watermark.title=????? ????? ?????
home.watermark.desc=??? ????? ????? ????? ??? ????? PDF ????? ??. home.watermark.desc=??? ????? ????? ????? ??? ????? PDF ????? ??.
watermark.tags=??,?????,?????,???,???? ?????,????? ??????,????,jpg,????,???? ?????????? watermark.tags=??,?????,?????,???,???? ?????,????? ??????,????,jpg,????,???? ??????????
@ -1642,3 +1646,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,16 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Verwischen
fakeScan.noise=Rauschen fakeScan.noise=Rauschen
fakeScan.yellowish=Gelblich (simulieren Sie altes Papier) fakeScan.yellowish=Gelblich (simulieren Sie altes Papier)
fakeScan.resolution=Auflösung (DPI) fakeScan.resolution=Auflösung (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -513,6 +513,10 @@ home.addImage.title=Add image
home.addImage.desc=Adds a image onto a set location on the PDF home.addImage.desc=Adds a image onto a set location on the PDF
addImage.tags=img,jpg,picture,photo addImage.tags=img,jpg,picture,photo
home.attachments.title=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.title=Add Watermark
home.watermark.desc=Add a custom watermark to your PDF document. home.watermark.desc=Add a custom watermark to your PDF document.
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
@ -1193,6 +1197,16 @@ addImage.everyPage=Every Page?
addImage.upload=Add image addImage.upload=Add image
addImage.submit=Add image addImage.submit=Add image
#attachments
attachments.title=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
merge.title=Merge merge.title=Merge

View File

@ -472,6 +472,10 @@ home.addImage.title=Add image
home.addImage.desc=Adds a image onto a set location on the PDF home.addImage.desc=Adds a image onto a set location on the PDF
addImage.tags=img,jpg,picture,photo addImage.tags=img,jpg,picture,photo
home.attachments.title=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.title=Add Watermark
home.watermark.desc=Add a custom watermark to your PDF document. home.watermark.desc=Add a custom watermark to your PDF document.
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
@ -480,7 +484,6 @@ home.permissions.title=Change Permissions
home.permissions.desc=Change the permissions of your PDF document home.permissions.desc=Change the permissions of your PDF document
permissions.tags=read,write,edit,print permissions.tags=read,write,edit,print
home.removePages.title=Remove home.removePages.title=Remove
home.removePages.desc=Delete unwanted pages from your PDF document. home.removePages.desc=Delete unwanted pages from your PDF document.
removePages.tags=Remove pages,delete pages removePages.tags=Remove pages,delete pages
@ -1153,6 +1156,18 @@ addImage.upload=Add image
addImage.submit=Add image addImage.submit=Add image
#attachments
attachments.title=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
merge.title=Merge merge.title=Merge
merge.header=Merge multiple PDFs (2+) merge.header=Merge multiple PDFs (2+)

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,16 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,17 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Elmos
fakeScan.noise=Zaj fakeScan.noise=Zaj
fakeScan.yellowish=Sárgás (régi papír szimulálása) fakeScan.yellowish=Sárgás (régi papír szimulálása)
fakeScan.resolution=Felbontás (DPI) fakeScan.resolution=Felbontás (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Sfocatura
fakeScan.noise=Rumore fakeScan.noise=Rumore
fakeScan.yellowish=Giallastro (simula carta vecchia) fakeScan.yellowish=Giallastro (simula carta vecchia)
fakeScan.resolution=Risoluzione (DPI) fakeScan.resolution=Risoluzione (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1475,3 +1475,17 @@ cookieBanner.preferencesModal.necessary.description=വെബ്സൈറ്റ
cookieBanner.preferencesModal.analytics.title=അനലിറ്റിക്സ് cookieBanner.preferencesModal.analytics.title=അനലിറ്റിക്സ്
cookieBanner.preferencesModal.analytics.description=ഞങ്ങളുടെ ടൂളുകൾ എങ്ങനെ ഉപയോഗിക്കുന്നുവെന്ന് മനസ്സിലാക്കാൻ ഈ കുക്കികൾ ഞങ്ങളെ സഹായിക്കുന്നു, അതിനാൽ ഞങ്ങളുടെ കമ്മ്യൂണിറ്റി ഏറ്റവും കൂടുതൽ വിലമതിക്കുന്ന ഫീച്ചറുകൾ നിർമ്മിക്കുന്നതിൽ ഞങ്ങൾക്ക് ശ്രദ്ധ കേന്ദ്രീകരിക്കാൻ കഴിയും. ഉറപ്പാക്കുക—സ്റ്റെർലിംഗ് PDF-ന് നിങ്ങൾ പ്രവർത്തിക്കുന്ന പ്രമാണങ്ങളുടെ ഉള്ളടക്കം ട്രാക്ക് ചെയ്യാൻ കഴിയില്ല, ഒരിക്കലും കഴിയില്ല. cookieBanner.preferencesModal.analytics.description=ഞങ്ങളുടെ ടൂളുകൾ എങ്ങനെ ഉപയോഗിക്കുന്നുവെന്ന് മനസ്സിലാക്കാൻ ഈ കുക്കികൾ ഞങ്ങളെ സഹായിക്കുന്നു, അതിനാൽ ഞങ്ങളുടെ കമ്മ്യൂണിറ്റി ഏറ്റവും കൂടുതൽ വിലമതിക്കുന്ന ഫീച്ചറുകൾ നിർമ്മിക്കുന്നതിൽ ഞങ്ങൾക്ക് ശ്രദ്ധ കേന്ദ്രീകരിക്കാൻ കഴിയും. ഉറപ്പാക്കുക—സ്റ്റെർലിംഗ് PDF-ന് നിങ്ങൾ പ്രവർത്തിക്കുന്ന പ്രമാണങ്ങളുടെ ഉള്ളടക്കം ട്രാക്ക് ചെയ്യാൻ കഴിയില്ല, ഒരിക്കലും കഴിയില്ല.
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,19 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

@ -1642,3 +1642,18 @@ fakeScan.blur=Blur
fakeScan.noise=Noise fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI) fakeScan.resolution=Resolution (DPI)
#attachments
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
attachments.title=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

View File

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

View File

@ -290,6 +290,9 @@
<div <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')}"> 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>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
</div>
</div> </div>
</div> </div>

View File

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

View File

@ -0,0 +1,221 @@
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,216 @@
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());
}
}
}

View File

@ -128,7 +128,7 @@ ui:
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
endpoints: # All the possible endpoints are disabled endpoints: # All the possible endpoints are disabled
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, add-attachments] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice']) groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
metrics: metrics:

View File

@ -30,6 +30,7 @@
/api/v1/misc/add-stamp /api/v1/misc/add-stamp
/api/v1/misc/add-page-numbers /api/v1/misc/add-page-numbers
/api/v1/misc/add-image /api/v1/misc/add-image
/api/v1/misc/add-attachments
/api/v1/convert/url/pdf /api/v1/convert/url/pdf
/api/v1/convert/pdf/xml /api/v1/convert/pdf/xml
/api/v1/convert/pdf/word /api/v1/convert/pdf/word

View File

@ -51,3 +51,4 @@
/swagger-ui/index.html /swagger-ui/index.html
/licenses /licenses
/releases /releases
/add-attachments