mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
Add Attachments Feature (#3781)
# Description of Changes Added a new feature to add attachments to a PDF document. ### Added: - `AttachmentController`: Endpoint for adding attachments at `/add-attachments` with parameters `fileInput` for the PDF and `attachments` as a list of files to attach - `AttachmentServiceInterface` - `AttachmentService`: Handles the logic of adding attachments to the PDF - `AttachmentUtils`: to handle setting the catalog viewer preferences in the viewer - Add Attachments page - Tests for new feature ### Changes: - `EmlToPdf`: Moved setting of viewer preferences to `AttachmentUtils` - `EndpointConfiguration: Included '/add-attachments' - Updated language files with attachments copy - General clean up Closes #1259 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [x] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [x] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable)     - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
committed by
GitHub
parent
8e8f0492c4
commit
32aa568196
@@ -0,0 +1,50 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PageMode;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class AttachmentUtils {
|
||||
|
||||
/**
|
||||
* Sets the PDF catalog viewer preferences to display attachments in the viewer.
|
||||
*
|
||||
* @param document The <code>PDDocument</code> to modify.
|
||||
* @param pageMode The <code>PageMode</code> to set for the PDF viewer. <code>PageMode</code>
|
||||
* values: <code>UseNone</code>, <code>UseOutlines</code>, <code>UseThumbs</code>, <code>
|
||||
* FullScreen</code>, <code>UseOC</code>, <code>UseAttachments</code>.
|
||||
*/
|
||||
public static void setCatalogViewerPreferences(PDDocument document, PageMode pageMode) {
|
||||
try {
|
||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||
if (catalog != null) {
|
||||
COSDictionary catalogDict = catalog.getCOSObject();
|
||||
|
||||
catalog.setPageMode(pageMode);
|
||||
catalogDict.setName(COSName.PAGE_MODE, pageMode.stringValue());
|
||||
|
||||
COSDictionary viewerPrefs =
|
||||
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
|
||||
if (viewerPrefs == null) {
|
||||
viewerPrefs = new COSDictionary();
|
||||
catalogDict.setItem(COSName.VIEWER_PREFERENCES, viewerPrefs);
|
||||
}
|
||||
|
||||
viewerPrefs.setName(
|
||||
COSName.getPDFName("NonFullScreenPageMode"), pageMode.stringValue());
|
||||
|
||||
viewerPrefs.setBoolean(COSName.getPDFName("DisplayDocTitle"), true);
|
||||
|
||||
log.info(
|
||||
"Set PDF PageMode to UseAttachments to automatically show attachments pane");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to set catalog viewer preferences for attachments", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -20,13 +22,11 @@ import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PageMode;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
||||
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||
@@ -42,10 +42,13 @@ import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class EmlToPdf {
|
||||
|
||||
private static final class StyleConstants {
|
||||
// Font and layout constants
|
||||
static final int DEFAULT_FONT_SIZE = 12;
|
||||
@@ -194,8 +197,7 @@ public class EmlToPdf {
|
||||
boolean disableSanitize)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest =
|
||||
createHtmlRequest(request);
|
||||
HTMLToPdfRequest htmlRequest = createHtmlRequest(request);
|
||||
|
||||
try {
|
||||
return FileToPdf.convertHtmlToPdf(
|
||||
@@ -879,33 +881,33 @@ public class EmlToPdf {
|
||||
Class<?> messageClass = message.getClass();
|
||||
|
||||
// Extract headers via reflection
|
||||
java.lang.reflect.Method getSubject = messageClass.getMethod("getSubject");
|
||||
Method getSubject = messageClass.getMethod("getSubject");
|
||||
String subject = (String) getSubject.invoke(message);
|
||||
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
|
||||
|
||||
java.lang.reflect.Method getFrom = messageClass.getMethod("getFrom");
|
||||
Method getFrom = messageClass.getMethod("getFrom");
|
||||
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
|
||||
content.setFrom(
|
||||
fromAddresses != null && fromAddresses.length > 0
|
||||
? safeMimeDecode(fromAddresses[0].toString())
|
||||
: "");
|
||||
|
||||
java.lang.reflect.Method getAllRecipients = messageClass.getMethod("getAllRecipients");
|
||||
Method getAllRecipients = messageClass.getMethod("getAllRecipients");
|
||||
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
|
||||
content.setTo(
|
||||
recipients != null && recipients.length > 0
|
||||
? safeMimeDecode(recipients[0].toString())
|
||||
: "");
|
||||
|
||||
java.lang.reflect.Method getSentDate = messageClass.getMethod("getSentDate");
|
||||
Method getSentDate = messageClass.getMethod("getSentDate");
|
||||
content.setDate((Date) getSentDate.invoke(message));
|
||||
|
||||
// Extract content
|
||||
java.lang.reflect.Method getContent = messageClass.getMethod("getContent");
|
||||
Method getContent = messageClass.getMethod("getContent");
|
||||
Object messageContent = getContent.invoke(message);
|
||||
|
||||
if (messageContent instanceof String stringContent) {
|
||||
java.lang.reflect.Method getContentType = messageClass.getMethod("getContentType");
|
||||
Method getContentType = messageClass.getMethod("getContentType");
|
||||
String contentType = (String) getContentType.invoke(message);
|
||||
if (contentType != null && contentType.toLowerCase().contains("text/html")) {
|
||||
content.setHtmlBody(stringContent);
|
||||
@@ -944,11 +946,10 @@ public class EmlToPdf {
|
||||
}
|
||||
|
||||
Class<?> multipartClass = multipart.getClass();
|
||||
java.lang.reflect.Method getCount = multipartClass.getMethod("getCount");
|
||||
Method getCount = multipartClass.getMethod("getCount");
|
||||
int count = (Integer) getCount.invoke(multipart);
|
||||
|
||||
java.lang.reflect.Method getBodyPart =
|
||||
multipartClass.getMethod("getBodyPart", int.class);
|
||||
Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
Object part = getBodyPart.invoke(multipart, i);
|
||||
@@ -969,12 +970,12 @@ public class EmlToPdf {
|
||||
}
|
||||
|
||||
Class<?> partClass = part.getClass();
|
||||
java.lang.reflect.Method isMimeType = partClass.getMethod("isMimeType", String.class);
|
||||
java.lang.reflect.Method getContent = partClass.getMethod("getContent");
|
||||
java.lang.reflect.Method getDisposition = partClass.getMethod("getDisposition");
|
||||
java.lang.reflect.Method getFileName = partClass.getMethod("getFileName");
|
||||
java.lang.reflect.Method getContentType = partClass.getMethod("getContentType");
|
||||
java.lang.reflect.Method getHeader = partClass.getMethod("getHeader", String.class);
|
||||
Method isMimeType = partClass.getMethod("isMimeType", String.class);
|
||||
Method getContent = partClass.getMethod("getContent");
|
||||
Method getDisposition = partClass.getMethod("getDisposition");
|
||||
Method getFileName = partClass.getMethod("getFileName");
|
||||
Method getContentType = partClass.getMethod("getContentType");
|
||||
Method getHeader = partClass.getMethod("getHeader", String.class);
|
||||
|
||||
Object disposition = getDisposition.invoke(part);
|
||||
String filename = (String) getFileName.invoke(part);
|
||||
@@ -1181,7 +1182,7 @@ public class EmlToPdf {
|
||||
private static byte[] attachFilesToPdf(
|
||||
byte[] pdfBytes,
|
||||
List<EmailAttachment> attachments,
|
||||
stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory)
|
||||
CustomPDFDocumentFactory pdfDocumentFactory)
|
||||
throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
@@ -1239,15 +1240,13 @@ public class EmlToPdf {
|
||||
document, new ByteArrayInputStream(attachment.getData()));
|
||||
embeddedFile.setSize(attachment.getData().length);
|
||||
embeddedFile.setCreationDate(new GregorianCalendar());
|
||||
if (attachment.getContentType() != null) {
|
||||
embeddedFile.setSubtype(attachment.getContentType());
|
||||
}
|
||||
|
||||
// Create file specification
|
||||
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
|
||||
fileSpec.setFile(uniqueFilename);
|
||||
fileSpec.setEmbeddedFile(embeddedFile);
|
||||
if (attachment.getContentType() != null) {
|
||||
embeddedFile.setSubtype(attachment.getContentType());
|
||||
fileSpec.setFileDescription("Email attachment: " + uniqueFilename);
|
||||
}
|
||||
|
||||
@@ -1269,7 +1268,7 @@ public class EmlToPdf {
|
||||
efTree.setNames(efMap);
|
||||
|
||||
// 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
|
||||
@@ -1423,41 +1422,7 @@ 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
|
||||
|
||||
private static String decodeMimeHeader(String encodedText) {
|
||||
if (encodedText == null || encodedText.trim().isEmpty()) {
|
||||
|
||||
@@ -16,12 +16,12 @@ import io.github.pixee.security.Filenames;
|
||||
|
||||
public class WebResponseUtils {
|
||||
|
||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
||||
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||
ByteArrayOutputStream baos, String docName) throws IOException {
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
||||
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||
ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
|
||||
}
|
||||
@@ -44,8 +44,7 @@ public class WebResponseUtils {
|
||||
headers.setContentType(mediaType);
|
||||
headers.setContentLength(bytes.length);
|
||||
String encodedDocName =
|
||||
URLEncoder.encode(docName, StandardCharsets.UTF_8.toString())
|
||||
.replaceAll("\\+", "%20");
|
||||
URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
headers.setContentDispositionFormData("attachment", encodedDocName);
|
||||
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
||||
}
|
||||
@@ -61,9 +60,8 @@ public class WebResponseUtils {
|
||||
// Open Byte Array and save document to it
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
// Close the document
|
||||
document.close();
|
||||
|
||||
return boasToWebResponse(baos, docName);
|
||||
return baosToWebResponse(baos, docName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class WebResponseUtilsTest {
|
||||
String docName = "sample.pdf";
|
||||
|
||||
ResponseEntity<byte[]> responseEntity =
|
||||
WebResponseUtils.boasToWebResponse(baos, docName);
|
||||
WebResponseUtils.baosToWebResponse(baos, docName);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
|
||||
Reference in New Issue
Block a user