mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-22 07:25:04 +00:00
Setting up AttachmentsController
This commit is contained in:
parent
bbaadc1822
commit
caef177ec3
@ -42,10 +42,12 @@ import lombok.experimental.UtilityClass;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
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
|
||||||
public class EmlToPdf {
|
public class EmlToPdf {
|
||||||
|
|
||||||
private static final class StyleConstants {
|
private static final class StyleConstants {
|
||||||
// Font and layout constants
|
// Font and layout constants
|
||||||
static final int DEFAULT_FONT_SIZE = 12;
|
static final int DEFAULT_FONT_SIZE = 12;
|
||||||
@ -1423,41 +1425,7 @@ public class EmlToPdf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setCatalogViewerPreferences(PDDocument document) {
|
// MIME header decoding functionality for RFC 2047 encoded headers - moved to constants
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String decodeMimeHeader(String encodedText) {
|
private static String decodeMimeHeader(String encodedText) {
|
||||||
if (encodedText == null || encodedText.trim().isEmpty()) {
|
if (encodedText == null || encodedText.trim().isEmpty()) {
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
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) {
|
||||||
|
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, PageMode.USE_ATTACHMENTS.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.USE_ATTACHMENTS.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
@ -64,6 +64,6 @@ public class WebResponseUtils {
|
|||||||
// Close the document
|
// Close the document
|
||||||
document.close();
|
document.close();
|
||||||
|
|
||||||
return boasToWebResponse(baos, docName);
|
return baosToWebResponse(baos, docName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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");
|
||||||
|
@ -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) {
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
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);
|
||||||
|
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
|
||||||
|
|
||||||
|
// Set PageMode to UseAttachments to show the attachments panel
|
||||||
|
catalog.setPageMode(PageMode.USE_ATTACHMENTS);
|
||||||
|
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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");
|
||||||
|
@ -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", "attachments");
|
||||||
|
return "misc/add-attachments";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
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.common.filespecification.PDComplexFileSpecification;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||||
|
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
|
||||||
|
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 void 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, PDComplexFileSpecification> existingEmbeddedFiles = existingNames;
|
||||||
|
attachments.forEach(
|
||||||
|
attachment -> {
|
||||||
|
// Create attachments specification
|
||||||
|
PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
|
||||||
|
fileSpecification.setFile(attachment.getOriginalFilename());
|
||||||
|
fileSpecification.setFileUnicode(attachment.getOriginalFilename());
|
||||||
|
fileSpecification.setFileDescription(
|
||||||
|
"Embedded attachment: " + attachment.getOriginalFilename());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create embedded attachment
|
||||||
|
PDEmbeddedFile embeddedFile =
|
||||||
|
new PDEmbeddedFile(document, attachment.getInputStream());
|
||||||
|
embeddedFile.setSize((int) attachment.getSize());
|
||||||
|
embeddedFile.setCreationDate(new GregorianCalendar());
|
||||||
|
embeddedFile.setModDate(new GregorianCalendar());
|
||||||
|
|
||||||
|
// Set MIME type if available
|
||||||
|
String contentType = attachment.getContentType();
|
||||||
|
if (StringUtils.isNotBlank(contentType)) {
|
||||||
|
embeddedFile.setSubtype(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate embedded attachment with file specification
|
||||||
|
embeddedFile.setFile(fileSpecification);
|
||||||
|
fileSpecification.setEmbeddedFile(embeddedFile);
|
||||||
|
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
|
||||||
|
|
||||||
|
// Add to the existing files map
|
||||||
|
existingEmbeddedFiles.put(
|
||||||
|
attachment.getOriginalFilename(), fileSpecification);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Added attachment: {} ({} bytes)",
|
||||||
|
attachment.getOriginalFilename(),
|
||||||
|
attachment.getSize());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create embedded file for attachment: {}",
|
||||||
|
attachment.getOriginalFilename(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
embeddedFilesTree.setNames(existingNames);
|
||||||
|
|
||||||
|
// Ensure document has proper access permissions for embedded files
|
||||||
|
grantAccessPermissions(document);
|
||||||
|
PDFAttachmentUtils.setCatalogViewerPreferences(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void grantAccessPermissions(PDDocument document) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
public interface PDFAttachmentServiceInterface {
|
||||||
|
|
||||||
|
void addAttachment(
|
||||||
|
PDDocument document,
|
||||||
|
PDEmbeddedFilesNameTreeNode efTree,
|
||||||
|
List<MultipartFile> attachments)
|
||||||
|
throws IOException;
|
||||||
|
}
|
@ -525,6 +525,10 @@ home.addImage.title=Add image
|
|||||||
home.addImage.desc=Adds a image onto a set location on the PDF
|
home.addImage.desc=Adds a image onto a set location on the PDF
|
||||||
addImage.tags=img,jpg,picture,photo
|
addImage.tags=img,jpg,picture,photo
|
||||||
|
|
||||||
|
home.attachments.title=Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=Add Watermark
|
home.watermark.title=Add Watermark
|
||||||
home.watermark.desc=Add a custom watermark to your PDF document.
|
home.watermark.desc=Add a custom watermark to your PDF document.
|
||||||
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
||||||
@ -1205,6 +1209,16 @@ addImage.everyPage=Every Page?
|
|||||||
addImage.upload=Add image
|
addImage.upload=Add image
|
||||||
addImage.submit=Add image
|
addImage.submit=Add image
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Attachments
|
||||||
|
attachments.header=Add attachments to PDF
|
||||||
|
attachments.removeHeader=Remove attachments from PDF
|
||||||
|
attachments.selectFiles=Select files to attach
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
attachments.removeDescription=This will remove all embedded files from the PDF.
|
||||||
|
attachments.removeButton=Remove All Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=Merge
|
merge.title=Merge
|
||||||
|
@ -525,6 +525,10 @@ home.addImage.title=Add image
|
|||||||
home.addImage.desc=Adds a image onto a set location on the PDF
|
home.addImage.desc=Adds a image onto a set location on the PDF
|
||||||
addImage.tags=img,jpg,picture,photo
|
addImage.tags=img,jpg,picture,photo
|
||||||
|
|
||||||
|
home.attachments.title=Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=Add Watermark
|
home.watermark.title=Add Watermark
|
||||||
home.watermark.desc=Add a custom watermark to your PDF document.
|
home.watermark.desc=Add a custom watermark to your PDF document.
|
||||||
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
||||||
@ -533,7 +537,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
|
||||||
@ -1206,6 +1209,18 @@ addImage.upload=Add image
|
|||||||
addImage.submit=Add image
|
addImage.submit=Add image
|
||||||
|
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Attachments
|
||||||
|
attachments.header=Add attachments to PDF
|
||||||
|
attachments.removeHeader=Remove attachments from PDF
|
||||||
|
attachments.selectFiles=Select files to attach
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
attachments.removeDescription=This will remove all embedded files from the PDF.
|
||||||
|
attachments.removeButton=Remove All Attachments
|
||||||
|
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=Merge
|
merge.title=Merge
|
||||||
merge.header=Merge multiple PDFs (2+)
|
merge.header=Merge multiple PDFs (2+)
|
||||||
|
@ -236,7 +236,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -51,3 +51,4 @@
|
|||||||
/swagger-ui/index.html
|
/swagger-ui/index.html
|
||||||
/licenses
|
/licenses
|
||||||
/releases
|
/releases
|
||||||
|
/add-attachments
|
||||||
|
Loading…
x
Reference in New Issue
Block a user