From b7a13ca84c0b431c48ffa52a24af07807270201a Mon Sep 17 00:00:00 2001 From: Frank Intorp Date: Mon, 21 Sep 2026 10:57:18 +0200 Subject: [PATCH] feat(pdf): expose PDF access permissions (print, copy, modify, ...) PDFBox already provides AccessPermission via PDDocument.getCurrentAccessPermission(), but PDFMatcher only exposed is-encrypted/security-handler/requires-user-password. Add a "permissions" entry to the PDF details map with the individual canPrint/canModify/canExtractContent/... flags. Co-Authored-By: Claude Sonnet 5 --- .../jadice/filetype/matchers/PDFMatcher.java | 44 +++++++++++++++++++ analyzer/src/test/java/TestPDFMatcher.java | 24 ++++++++++ 2 files changed, 68 insertions(+) diff --git a/analyzer/src/main/java/org/jadice/filetype/matchers/PDFMatcher.java b/analyzer/src/main/java/org/jadice/filetype/matchers/PDFMatcher.java index c1f3852..06f5eb0 100644 --- a/analyzer/src/main/java/org/jadice/filetype/matchers/PDFMatcher.java +++ b/analyzer/src/main/java/org/jadice/filetype/matchers/PDFMatcher.java @@ -34,6 +34,7 @@ import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.PDEncryption; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; @@ -67,6 +68,26 @@ public class PDFMatcher extends Matcher { public static final String IS_ENCRYPTED_KEY = "is-encrypted"; + public static final String PERMISSIONS_KEY = "permissions"; + + public static final String CAN_PRINT_KEY = "can-print"; + + public static final String CAN_PRINT_FAITHFUL_KEY = "can-print-faithful"; + + public static final String CAN_MODIFY_KEY = "can-modify"; + + public static final String CAN_MODIFY_ANNOTATIONS_KEY = "can-modify-annotations"; + + public static final String CAN_FILL_IN_FORM_KEY = "can-fill-in-form"; + + public static final String CAN_EXTRACT_CONTENT_KEY = "can-extract-content"; + + public static final String CAN_EXTRACT_FOR_ACCESSIBILITY_KEY = "can-extract-for-accessibility"; + + public static final String CAN_ASSEMBLE_DOCUMENT_KEY = "can-assemble-document"; + + public static final String IS_OWNER_PERMISSION_KEY = "is-owner-permission"; + public static final String METADATA_KEY = "metadata"; public static final String VERSION_KEY = "version"; @@ -121,6 +142,7 @@ public boolean matches(final Context context) { } else pdfDetails.put(IS_ENCRYPTED_KEY, false); + providePermissionInfo(pdfDetails, document.getCurrentAccessPermission()); final List filenames = new ArrayList<>(); @@ -165,6 +187,28 @@ private void provideDocumentInformation(final Map pdfDetails, fi pdfDetails.put("trapped", info.getTrapped()); } + /** + * Adds the PDF's access permissions (print, copy, modify, fill-in-form, etc.) to the result map. + * These are available regardless of whether the document is encrypted: for unencrypted + * documents PDFBox reports the permissions the owner would have (i.e. everything allowed). + * + * @param pdfDetails map to which the results get added + * @param permission the document's current access permission + */ + private void providePermissionInfo(final Map pdfDetails, final AccessPermission permission) { + Map permissions = new HashMap<>(); + permissions.put(CAN_PRINT_KEY, permission.canPrint()); + permissions.put(CAN_PRINT_FAITHFUL_KEY, permission.canPrintFaithful()); + permissions.put(CAN_MODIFY_KEY, permission.canModify()); + permissions.put(CAN_MODIFY_ANNOTATIONS_KEY, permission.canModifyAnnotations()); + permissions.put(CAN_FILL_IN_FORM_KEY, permission.canFillInForm()); + permissions.put(CAN_EXTRACT_CONTENT_KEY, permission.canExtractContent()); + permissions.put(CAN_EXTRACT_FOR_ACCESSIBILITY_KEY, permission.canExtractForAccessibility()); + permissions.put(CAN_ASSEMBLE_DOCUMENT_KEY, permission.canAssembleDocument()); + permissions.put(IS_OWNER_PERMISSION_KEY, permission.isOwnerPermission()); + pdfDetails.put(PERMISSIONS_KEY, permissions); + } + private void provideXMPMetadata(final Map pdfDetails, final PDMetadata meta) throws IOException { InputStream xmpMetadata = meta.exportXMPMetadata(); diff --git a/analyzer/src/test/java/TestPDFMatcher.java b/analyzer/src/test/java/TestPDFMatcher.java index a077656..50647ef 100644 --- a/analyzer/src/test/java/TestPDFMatcher.java +++ b/analyzer/src/test/java/TestPDFMatcher.java @@ -51,6 +51,7 @@ void testUnencryptedPDFs() throws IOException { assertValidDetails(result); assertFalse(isEncrypted(result), f + " is not recognized as unencrypted PDF"); assertFalse(hasEmbeddedDocuments(result), f + " has no embedded documents"); + assertValidPermissions(result); } } @@ -80,6 +81,7 @@ void testPDFPortfolios() throws IOException { assertValidDetails(result); assertFalse(isEncrypted(result), f + " is not recognized as unencrypted PDF"); assertTrue(hasEmbeddedDocuments(result), f + " has embedded documents, but none were found"); + assertValidPermissions(result); } } @@ -94,6 +96,7 @@ void testEncryptedPDFs() throws IOException { assertValidDetails(result); assertTrue(isEncrypted(result),f + " is not recognized as encrypted PDF"); assertFalse(hasEmbeddedDocuments(result), f + " has no embedded documents"); + assertValidPermissions(result); } } @@ -205,6 +208,27 @@ private static void assertValidDetails(final Map result) { assertTrue(object instanceof Map, "PDF details are not a map, but " + object.getClass()); } + @SuppressWarnings("unchecked") + private static void assertValidPermissions(final Map result) { + final Map details = (Map) result.get(PDFMatcher.DETAILS_KEY); + assertThat(details, hasKey(PDFMatcher.PERMISSIONS_KEY)); + final Object permissionsObject = details.get(PDFMatcher.PERMISSIONS_KEY); + assertTrue(permissionsObject instanceof Map, "PDF permissions are not a map, but " + permissionsObject.getClass()); + final Map permissions = (Map) permissionsObject; + assertThat(permissions, hasKey(PDFMatcher.CAN_PRINT_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_PRINT_FAITHFUL_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_MODIFY_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_MODIFY_ANNOTATIONS_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_FILL_IN_FORM_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_EXTRACT_CONTENT_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_EXTRACT_FOR_ACCESSIBILITY_KEY)); + assertThat(permissions, hasKey(PDFMatcher.CAN_ASSEMBLE_DOCUMENT_KEY)); + assertThat(permissions, hasKey(PDFMatcher.IS_OWNER_PERMISSION_KEY)); + for (Object value : permissions.values()) { + assertTrue(value instanceof Boolean, "Permission value is not a boolean, but " + value.getClass()); + } + } + @SuppressWarnings("unchecked") private static boolean isEncrypted(final Map result) { final Map details = (Map) result.get(PDFMatcher.DETAILS_KEY);