diff --git a/ChangeLog b/ChangeLog index be8d2564..f587b967 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,8 @@ 2021-02-10 Jay Berkenbilt + * Add "attachments" as an additional json key, and add some + information about attachments to the json output. + * Add new command-line arguments for operating on attachments: --list-attachments, --add-attachment, --remove-attachment, --copy-attachments-from. See --help and manual for details. diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 5205028d..6885aff6 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -5104,6 +5104,19 @@ print "\n"; than using for this purpose. + + + Add some information about attachments to the json output, + and added attachments as an additional + json key. The information included here is limited to the + preferred name and content stream and a reference to the + file spec object. This is enough detail for clients to avoid + the hassle of navigating a name tree and provides what is + needed for basic enumeration and extraction of attachments. + More detailed information can be obtained by following the + reference to the file spec object. + + diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index d23fad09..e41be64b 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -699,6 +699,22 @@ static JSON json_schema(std::set* keys = 0) "filemethod", JSON::makeString("encryption method for attachments")); } + if (all_keys || keys->count("attachments")) + { + JSON attachments = schema.addDictionaryMember( + "attachments", JSON::makeDictionary()); + JSON details = attachments.addDictionaryMember( + "", JSON::makeDictionary()); + details.addDictionaryMember( + "filespec", + JSON::makeString("object containing the file spec")); + details.addDictionaryMember( + "preferredname", + JSON::makeString("most preferred file name")); + details.addDictionaryMember( + "preferredcontents", + JSON::makeString("most preferred embedded file stream")); + } return schema; } @@ -1114,7 +1130,7 @@ ArgParser::initOptionTable() // places: json_schema, do_json, and initOptionTable. char const* json_key_choices[] = { "objects", "objectinfo", "pages", "pagelabels", "outlines", - "acroform", "encrypt", 0}; + "acroform", "encrypt", "attachments", 0}; (*t)["json-key"] = oe_requiredChoices( &ArgParser::argJsonKey, json_key_choices); (*t)["json-object"] = oe_requiredParameter( @@ -4568,6 +4584,28 @@ static void do_json_encrypt(QPDF& pdf, Options& o, JSON& j) "filemethod", JSON::makeString(s_file_method)); } +static void do_json_attachments(QPDF& pdf, Options& o, JSON& j) +{ + JSON j_attachments = j.addDictionaryMember( + "attachments", JSON::makeDictionary()); + QPDFEmbeddedFileDocumentHelper efdh(pdf); + for (auto const& iter: efdh.getEmbeddedFiles()) + { + std::string const& key = iter.first; + auto fsoh = iter.second; + auto j_details = j_attachments.addDictionaryMember( + key, JSON::makeDictionary()); + j_details.addDictionaryMember( + "filespec", + JSON::makeString(fsoh->getObjectHandle().unparse())); + j_details.addDictionaryMember( + "preferredname", JSON::makeString(fsoh->getFilename())); + j_details.addDictionaryMember( + "preferredcontents", + JSON::makeString(fsoh->getEmbeddedFileStream().unparse())); + } +} + static void do_json(QPDF& pdf, Options& o) { JSON j = JSON::makeDictionary(); @@ -4628,6 +4666,10 @@ static void do_json(QPDF& pdf, Options& o) { do_json_encrypt(pdf, o, j); } + if (all_keys || o.json_keys.count("attachments")) + { + do_json_attachments(pdf, o, j); + } // Check against schema diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 348d3948..dba10181 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -523,7 +523,7 @@ $td->runtest("page operations on form xobject", show_ntests(); # ---------- $td->notify("--- File Attachments ---"); -$n_tests += 33; +$n_tests += 34; open(F, ">auto-txt") or die; print F "from file"; @@ -547,6 +547,10 @@ $td->runtest("list attachments verbose", {$td->COMMAND => "qpdf --list-attachments --verbose a.pdf"}, {$td->FILE => "test76-list-verbose.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("attachments json", + {$td->COMMAND => "qpdf --json --json-key=attachments a.pdf"}, + {$td->FILE => "test76-json.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); $td->runtest("remove attachment (test_driver)", {$td->COMMAND => "test_driver 77 test76.pdf"}, {$td->STRING => "test 77 done\n", $td->EXIT_STATUS => 0}, diff --git a/qpdf/qtest/qpdf/direct-pages-json.out b/qpdf/qtest/qpdf/direct-pages-json.out index 0bd43563..52e5e2dd 100644 --- a/qpdf/qtest/qpdf/direct-pages-json.out +++ b/qpdf/qtest/qpdf/direct-pages-json.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-field-types---show-encryption-key.out b/qpdf/qtest/qpdf/json-field-types---show-encryption-key.out index 7bc40bdb..ad9c2003 100644 --- a/qpdf/qtest/qpdf/json-field-types---show-encryption-key.out +++ b/qpdf/qtest/qpdf/json-field-types---show-encryption-key.out @@ -385,6 +385,7 @@ "hasacroform": true, "needappearances": true }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-field-types.out b/qpdf/qtest/qpdf/json-field-types.out index 7bc40bdb..ad9c2003 100644 --- a/qpdf/qtest/qpdf/json-field-types.out +++ b/qpdf/qtest/qpdf/json-field-types.out @@ -385,6 +385,7 @@ "hasacroform": true, "needappearances": true }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-image-streams-all.out b/qpdf/qtest/qpdf/json-image-streams-all.out index 8eaa4583..3dea8852 100644 --- a/qpdf/qtest/qpdf/json-image-streams-all.out +++ b/qpdf/qtest/qpdf/json-image-streams-all.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-image-streams-small.out b/qpdf/qtest/qpdf/json-image-streams-small.out index dd7935ee..92d0c4f3 100644 --- a/qpdf/qtest/qpdf/json-image-streams-small.out +++ b/qpdf/qtest/qpdf/json-image-streams-small.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-image-streams-specialized.out b/qpdf/qtest/qpdf/json-image-streams-specialized.out index 9c6567f5..c342f9e6 100644 --- a/qpdf/qtest/qpdf/json-image-streams-specialized.out +++ b/qpdf/qtest/qpdf/json-image-streams-specialized.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-image-streams.out b/qpdf/qtest/qpdf/json-image-streams.out index 734868a5..2cfbd531 100644 --- a/qpdf/qtest/qpdf/json-image-streams.out +++ b/qpdf/qtest/qpdf/json-image-streams.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-outlines-with-actions.out b/qpdf/qtest/qpdf/json-outlines-with-actions.out index 25776abe..9755d0b8 100644 --- a/qpdf/qtest/qpdf/json-outlines-with-actions.out +++ b/qpdf/qtest/qpdf/json-outlines-with-actions.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-outlines-with-old-root-dests.out b/qpdf/qtest/qpdf/json-outlines-with-old-root-dests.out index 11735197..af3ce99c 100644 --- a/qpdf/qtest/qpdf/json-outlines-with-old-root-dests.out +++ b/qpdf/qtest/qpdf/json-outlines-with-old-root-dests.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-page-labels-and-outlines.out b/qpdf/qtest/qpdf/json-page-labels-and-outlines.out index b0de1c1d..e7d702f6 100644 --- a/qpdf/qtest/qpdf/json-page-labels-and-outlines.out +++ b/qpdf/qtest/qpdf/json-page-labels-and-outlines.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/json-page-labels-num-tree.out b/qpdf/qtest/qpdf/json-page-labels-num-tree.out index 497f428c..d0f73a61 100644 --- a/qpdf/qtest/qpdf/json-page-labels-num-tree.out +++ b/qpdf/qtest/qpdf/json-page-labels-num-tree.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/page_api_2-json.out b/qpdf/qtest/qpdf/page_api_2-json.out index b8ce4f38..172ce1c1 100644 --- a/qpdf/qtest/qpdf/page_api_2-json.out +++ b/qpdf/qtest/qpdf/page_api_2-json.out @@ -4,6 +4,7 @@ "hasacroform": false, "needappearances": false }, + "attachments": {}, "encrypt": { "capabilities": { "accessibility": true, diff --git a/qpdf/qtest/qpdf/test76-json.out b/qpdf/qtest/qpdf/test76-json.out new file mode 100644 index 00000000..d1c88cf8 --- /dev/null +++ b/qpdf/qtest/qpdf/test76-json.out @@ -0,0 +1,23 @@ +{ + "attachments": { + "att1": { + "filespec": "4 0 R", + "preferredcontents": "8 0 R", + "preferredname": "att1.txt" + }, + "att2": { + "filespec": "5 0 R", + "preferredcontents": "10 0 R", + "preferredname": "att2.txt" + }, + "att3": { + "filespec": "6 0 R", + "preferredcontents": "12 0 R", + "preferredname": "π.txt" + } + }, + "parameters": { + "decodelevel": "generalized" + }, + "version": 1 +}