diff --git a/common/src/main/java/stirling/software/common/util/EmlToPdf.java b/common/src/main/java/stirling/software/common/util/EmlToPdf.java index b08bc16a5..fe7638fb9 100644 --- a/common/src/main/java/stirling/software/common/util/EmlToPdf.java +++ b/common/src/main/java/stirling/software/common/util/EmlToPdf.java @@ -42,10 +42,12 @@ import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.api.converters.EmlToPdfRequest; +import static stirling.software.common.util.PDFAttachmentUtils.setCatalogViewerPreferences; @Slf4j @UtilityClass public class EmlToPdf { + private static final class StyleConstants { // Font and layout constants static final int DEFAULT_FONT_SIZE = 12; @@ -1423,41 +1425,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()) { diff --git a/common/src/main/java/stirling/software/common/util/PDFAttachmentUtils.java b/common/src/main/java/stirling/software/common/util/PDFAttachmentUtils.java new file mode 100644 index 000000000..a20c1fb00 --- /dev/null +++ b/common/src/main/java/stirling/software/common/util/PDFAttachmentUtils.java @@ -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); + } + } +} diff --git a/common/src/main/java/stirling/software/common/util/WebResponseUtils.java b/common/src/main/java/stirling/software/common/util/WebResponseUtils.java index 62a0e3246..c96ff16b1 100644 --- a/common/src/main/java/stirling/software/common/util/WebResponseUtils.java +++ b/common/src/main/java/stirling/software/common/util/WebResponseUtils.java @@ -16,12 +16,12 @@ import io.github.pixee.security.Filenames; public class WebResponseUtils { - public static ResponseEntity boasToWebResponse( + public static ResponseEntity baosToWebResponse( ByteArrayOutputStream baos, String docName) throws IOException { return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); } - public static ResponseEntity boasToWebResponse( + public static ResponseEntity baosToWebResponse( ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); } @@ -44,7 +44,7 @@ public class WebResponseUtils { headers.setContentType(mediaType); headers.setContentLength(bytes.length); String encodedDocName = - URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()) + URLEncoder.encode(docName, StandardCharsets.UTF_8) .replaceAll("\\+", "%20"); headers.setContentDispositionFormData("attachment", encodedDocName); return new ResponseEntity<>(bytes, headers, HttpStatus.OK); @@ -64,6 +64,6 @@ public class WebResponseUtils { // Close the document document.close(); - return boasToWebResponse(baos, docName); + return baosToWebResponse(baos, docName); } } diff --git a/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java b/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java index f5ce5a6b1..70286fbf7 100644 --- a/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java +++ b/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java @@ -25,7 +25,7 @@ public class WebResponseUtilsTest { String docName = "sample.pdf"; ResponseEntity responseEntity = - WebResponseUtils.boasToWebResponse(baos, docName); + WebResponseUtils.baosToWebResponse(baos, docName); assertNotNull(responseEntity); assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 5e37314a6..ddd988ef9 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -225,7 +225,7 @@ public class MergeController { String mergedFileName = files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged_unsigned.pdf"; - return WebResponseUtils.boasToWebResponse( + return WebResponseUtils.baosToWebResponse( baos, mergedFileName); // Return the modified PDF } catch (Exception ex) { diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index a7314fc7e..7d5086b4c 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -144,7 +144,7 @@ public class BlankPageController { zos.close(); log.info("Returning ZIP file: {}", filename + "_processed.zip"); - return WebResponseUtils.boasToWebResponse( + return WebResponseUtils.baosToWebResponse( baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM); } catch (IOException e) { diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 4ec844485..cb06b9f4d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -148,7 +148,7 @@ public class ExtractImagesController { // Create ByteArrayResource from byte array byte[] zipContents = baos.toByteArray(); - return WebResponseUtils.boasToWebResponse( + return WebResponseUtils.baosToWebResponse( baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index d573301d0..d6b4fa0da 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -118,7 +118,7 @@ public class PipelineController { } zipOut.close(); log.info("Returning zipped file response..."); - return WebResponseUtils.boasToWebResponse( + return WebResponseUtils.baosToWebResponse( baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); } catch (Exception e) { log.error("Error handling data: ", e); diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 3260eb31f..612c666c4 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -205,7 +205,7 @@ public class CertSignController { location, reason, showLogo); - return WebResponseUtils.boasToWebResponse( + return WebResponseUtils.baosToWebResponse( baos, Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") + "_signed.pdf"); diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/service/PDFAttachmentService.java b/stirling-pdf/src/main/java/stirling/software/SPDF/service/PDFAttachmentService.java index a5a3f2a4a..6c5bcfad1 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/service/PDFAttachmentService.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/service/PDFAttachmentService.java @@ -1,6 +1,8 @@ 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; @@ -15,6 +17,8 @@ import org.springframework.web.multipart.MultipartFile; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.util.PDFAttachmentUtils; + @Slf4j @Service public class PDFAttachmentService implements PDFAttachmentServiceInterface { @@ -31,7 +35,7 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface { existingNames = embeddedFilesTree.getNames(); if (existingNames == null) { log.debug("No existing embedded files found, creating new names map."); - existingNames = new java.util.HashMap<>(); + existingNames = new HashMap<>(); } log.debug("Embedded files: {}", existingNames.keySet()); @@ -55,9 +59,8 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface { PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, attachment.getInputStream()); embeddedFile.setSize((int) attachment.getSize()); - embeddedFile.setCreationDate(new java.util.GregorianCalendar()); - embeddedFile.setFile(fileSpecification); - embeddedFile.setModDate(new java.util.GregorianCalendar()); + embeddedFile.setCreationDate(new GregorianCalendar()); + embeddedFile.setModDate(new GregorianCalendar()); // Set MIME type if available String contentType = attachment.getContentType(); @@ -66,10 +69,11 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface { } // Associate embedded attachment with file specification + embeddedFile.setFile(fileSpecification); fileSpecification.setEmbeddedFile(embeddedFile); fileSpecification.setEmbeddedFileUnicode(embeddedFile); - // Add to the existing names map + // Add to the existing files map existingEmbeddedFiles.put( attachment.getOriginalFilename(), fileSpecification); @@ -89,6 +93,7 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface { // Ensure document has proper access permissions for embedded files grantAccessPermissions(document); + PDFAttachmentUtils.setCatalogViewerPreferences(document); } private void grantAccessPermissions(PDDocument document) {