diff --git a/ChangeLog b/ChangeLog index 0511ca53..be8d2564 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2021-02-10 Jay Berkenbilt + + * Add new command-line arguments for operating on attachments: + --list-attachments, --add-attachment, --remove-attachment, + --copy-attachments-from. See --help and manual for details. + 2021-02-09 Jay Berkenbilt * Add methods to QUtil for working with PDF timestamp strings: diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 174883a7..5205028d 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -1801,6 +1801,181 @@ outfile.pdf + + Embedded Files/Attachments Options + + Starting with qpdf 10.2, you can work with file attachments in PDF + files from the command line. The following options are available: + + + + + + Show the “key” and stream number for embedded + files. With , additional + information, including preferred file name, description, + dates, and more are also displayed. The key is usually but not + always equal to the file name, and is needed by some of the + other options. + + + + + + + + Write the contents of the specified attachment to standard + output as binary data. The key should match one of the keys + shown by . If specified + multiple times, only the last attachment will be shown. + + + + + + + + Add or replace an attachment with the contents of + file. This may be specified more + than once. The following additional options may appear before + the -- that ends this option: + + + + + + The key to use to register the attachment in the embedded + files table. Defaults to the last path element of + file. + + + + + + + + The file name to be used for the attachment. This is what is usually + displayed to the user and is the name most graphical PDF + viewers will use when saving a file. It defaults to the + last path element of file. + + + + + + + + The attachment's creation date in PDF format; defaults to + the current time. The date format is explained below. + + + + + + + + The attachment's modification date in PDF format; defaults + to the current time. The date format is explained below. + + + + + + + + The mime type for the attachment, e.g. + text/plain or + application/pdf. Note that the mimetype + appears in a field called /Subtype in + the PDF but actually includes the full type and subtype of + the mime type. + + + + + + + + Descriptive text for the attachment, displayed by some PDF + viewers. + + + + + + + + Indicates that any existing attachment with the same key + should be replaced by the new attachment. Otherwise, + qpdf gives an error if an attachment + with that key is already present. + + + + + + + + + + + + Remove the specified attachment. This doesn't only remove the + attachment from the embedded files table but also clears out + the file specification. That means that any potential internal + links to the attachment will be broken. This option may be + specified multiple times. Run with + to see status of the removal. + + + + + + + + Copy attachments from another file. This may be specified more + than once. The following additional options may appear before + the -- that ends this option: + + + + + + If required, the password needed to open + file + + + + + + + + Only required if the file from which attachments are being + copied has attachments with keys that conflict with + attachments already in the file. In this case, the + specified prefix will be prepended to each key. This + affects only the key in the embedded files table, not the + file name. The PDF specification doesn't preclude multiple + attachments having the same file name. + + + + + + + + + When a date is required, the date should conform to the PDF date + format specification, which is + D:yyyymmddhhmmss<z>, + where <z> is either + Z for UTC or a timezone offset in the form + -hh'mm' or + +hh'mm'. Examples: + D:20210207161528-05'00', + D:20210207211528Z. + + Advanced Parsing Options @@ -4911,6 +5086,13 @@ print "\n"; CLI Enhancements + + + Add new command line options for listing, saving, adding, + removing, and and copying file attachments. See for details. + + The option diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 7138bd73..d23fad09 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -95,6 +96,31 @@ struct UnderOverlay std::vector repeat_pagenos; }; +struct AddAttachment +{ + AddAttachment() : + replace(false) + { + } + + std::string path; + std::string key; + std::string filename; + std::string creationdate; + std::string moddate; + std::string mimetype; + std::string description; + bool replace; +}; + +struct CopyAttachmentFrom +{ + std::string path; + std::string password; + std::string prefix; +}; + + enum remove_unref_e { re_auto, re_yes, re_no }; struct Options @@ -177,6 +203,7 @@ struct Options show_page_images(false), collate(false), flatten_rotation(false), + list_attachments(false), json(false), check(false), optimize_images(false), @@ -282,6 +309,11 @@ struct Options bool show_page_images; bool collate; bool flatten_rotation; + bool list_attachments; + std::string attachment_to_show; + std::list attachments_to_remove; + std::list attachments_to_add; + std::list attachments_to_copy; bool json; std::set json_keys; std::set json_objects; @@ -758,6 +790,11 @@ class ArgParser void argRotate(char* parameter); void argCollate(); void argFlattenRotation(); + void argListAttachments(); + void argShowAttachment(char* parameter); + void argRemoveAttachment(char* parameter); + void argAddAttachment(); + void argCopyAttachments(); void argStreamData(char* parameter); void argCompressStreams(char* parameter); void argRecompressFlate(); @@ -838,6 +875,19 @@ class ArgParser void argReplaceInput(); void argIsEncrypted(); void argRequiresPassword(); + void argAApositional(char* arg); + void argAAKey(char* parameter); + void argAAFilename(char* parameter); + void argAACreationDate(char* parameter); + void argAAModDate(char* parameter); + void argAAMimeType(char* parameter); + void argAADescription(char* parameter); + void argAAReplace(); + void argEndAddAttachment(); + void argCApositional(char* arg); + void argCAprefix(char* parameter); + void argCApassword(char* parameter); + void argEndCopyAttachments(); void usage(std::string const& message); void checkCompletion(); @@ -874,6 +924,8 @@ class ArgParser std::map encrypt128_option_table; std::map encrypt256_option_table; std::map under_overlay_option_table; + std::map add_attachment_option_table; + std::map copy_attachments_option_table; std::vector > new_argv; std::vector > bash_argv; PointerHolder argv_ph; @@ -982,6 +1034,13 @@ ArgParser::initOptionTable() {"compress", "preserve", "uncompress", 0}; (*t)["collate"] = oe_bare(&ArgParser::argCollate); (*t)["flatten-rotation"] = oe_bare(&ArgParser::argFlattenRotation); + (*t)["list-attachments"] = oe_bare(&ArgParser::argListAttachments); + (*t)["show-attachment"] = oe_requiredParameter( + &ArgParser::argShowAttachment, "attachment-key"); + (*t)["remove-attachment"] = oe_requiredParameter( + &ArgParser::argRemoveAttachment, "attachment-key"); + (*t)["add-attachment"] = oe_bare(&ArgParser::argAddAttachment); + (*t)["copy-attachments-from"] = oe_bare(&ArgParser::argCopyAttachments); (*t)["stream-data"] = oe_requiredChoices( &ArgParser::argStreamData, stream_data_choices); (*t)["compress-streams"] = oe_requiredChoices( @@ -1129,6 +1188,31 @@ ArgParser::initOptionTable() (*t)["password"] = oe_requiredParameter( &ArgParser::argUOpassword, "password"); (*t)["--"] = oe_bare(&ArgParser::argEndUnderOverlay); + + t = &this->add_attachment_option_table; + (*t)[""] = oe_positional(&ArgParser::argAApositional); + (*t)["key"] = oe_requiredParameter( + &ArgParser::argAAKey, "attachment-key"); + (*t)["filename"] = oe_requiredParameter( + &ArgParser::argAAFilename, "filename"); + (*t)["creationdate"] = oe_requiredParameter( + &ArgParser::argAACreationDate, "creation-date"); + (*t)["moddate"] = oe_requiredParameter( + &ArgParser::argAAModDate, "modification-date"); + (*t)["mimetype"] = oe_requiredParameter( + &ArgParser::argAAMimeType, "mime/type"); + (*t)["description"] = oe_requiredParameter( + &ArgParser::argAADescription, "description"); + (*t)["replace"] = oe_bare(&ArgParser::argAAReplace); + (*t)["--"] = oe_bare(&ArgParser::argEndAddAttachment); + + t = &this->copy_attachments_option_table; + (*t)[""] = oe_positional(&ArgParser::argCApositional); + (*t)["prefix"] = oe_requiredParameter( + &ArgParser::argCAprefix, "prefix"); + (*t)["password"] = oe_requiredParameter( + &ArgParser::argCApassword, "password"); + (*t)["--"] = oe_bare(&ArgParser::argEndCopyAttachments); } void @@ -1361,7 +1445,6 @@ ArgParser::argHelp() << " --allow-insecure allow the owner password to be empty when the\n" << " user password is not empty\n" << "\n" - << "\n" << " print-opt may be:\n" << "\n" << " full allow full printing\n" @@ -1487,6 +1570,55 @@ ArgParser::argHelp() << " any \"from\" pages have been exhausted\n" << "\n" << "\n" + << "Embedded Files/Attachments Options\n" + << "----------------------------------\n" + << "\n" + << "These options can be used to work with embedded files, also known as\n" + << "attachments.\n" + << "\n" + << "--list-attachments show key and stream number for embedded files;\n" + << " combine with --verbose for more detailed information\n" + << "--show-attachment=key write the contents of the specified attachment to\n" + << " standard output as binary data\n" + << "--add-attachment file options --\n" + << " add or replace an attachment\n" + << "--remove-attachment=key remove the specified attachment; repeatable\n" + << "--copy-attachments-from file options --\n" + << " copy attachments from another file\n" + << "\n" + << "The \"key\" option is the unique name under which the attachment is registered\n" + << "within the PDF file. You can get this using the --list-attachments option. This\n" + << "is usually the same as the filename, but it doesn't have to be.\n" + << "\n" + << "Options for adding attachments:\n" + << "\n" + << " file path to the file to attach\n" + << " --key=key the name of this in the embedded files table;\n" + << " defaults to the last path element of file\n" + << " --filename=name the file name of the attachment; this is what is\n" + << " usually displayed to the user; defaults to the\n" + << " last path element of file\n" + << " --creationdate=date creation date in PDF format; defaults to the\n" + << " current time\n" + << " --moddate=date modification date in PDF format; defaults to the\n" + << " current time\n" + << " --mimetype=type/subtype mime type of attachment (e.g. application/pdf)\n" + << " --description=\"text\" attachment description\n" + << " --replace replace any existing attachment with the same key\n" + << "\n" + << "Options for copying attachments:\n" + << "\n" + << " file file whose attachments should be copied\n" + << " --password=password password to open the other file, if needed\n" + << " --prefix=prefix a prefix to insert in front of each key;\n" + << " required if needed to ensure each attachment\n" + << " has a unique key\n" + << "\n" + << "Date format: D:yyyymmddhhmmss where is either Z for UTC or a timezone\n" + << "offset in the form -hh'mm' or +hh'mm'.\n" + << "Examples: D:20210207161528-05'00', D:20210207211528Z\n" + << "\n" + << "\n" << "Advanced Parsing Options\n" << "------------------------\n" << "\n" @@ -1960,6 +2092,40 @@ ArgParser::argFlattenRotation() o.flatten_rotation = true; } +void +ArgParser::argListAttachments() +{ + o.list_attachments = true; + o.require_outfile = false; +} + +void +ArgParser::argShowAttachment(char* parameter) +{ + o.attachment_to_show = parameter; + o.require_outfile = false; +} + +void +ArgParser::argRemoveAttachment(char* parameter) +{ + o.attachments_to_remove.push_back(parameter); +} + +void +ArgParser::argAddAttachment() +{ + this->option_table = &(this->add_attachment_option_table); + o.attachments_to_add.push_back(AddAttachment()); +} + +void +ArgParser::argCopyAttachments() +{ + this->option_table = &(this->copy_attachments_option_table); + o.attachments_to_copy.push_back(CopyAttachmentFrom()); +} + void ArgParser::argStreamData(char* parameter) { @@ -2617,6 +2783,134 @@ ArgParser::argRequiresPassword() o.require_outfile = false; } +void +ArgParser::argAApositional(char* arg) +{ + o.attachments_to_add.back().path = arg; +} + +void +ArgParser::argAAKey(char* parameter) +{ + o.attachments_to_add.back().key = parameter; +} + +void +ArgParser::argAAFilename(char* parameter) +{ + o.attachments_to_add.back().filename = parameter; +} + +void +ArgParser::argAACreationDate(char* parameter) +{ + if (! QUtil::pdf_time_to_qpdf_time(parameter)) + { + usage(std::string(parameter) + " is not a valid PDF timestamp"); + } + o.attachments_to_add.back().creationdate = parameter; +} + +void +ArgParser::argAAModDate(char* parameter) +{ + if (! QUtil::pdf_time_to_qpdf_time(parameter)) + { + usage(std::string(parameter) + " is not a valid PDF timestamp"); + } + o.attachments_to_add.back().moddate = parameter; +} + +void +ArgParser::argAAMimeType(char* parameter) +{ + if (strchr(parameter, '/') == nullptr) + { + usage("mime type should be specified as type/subtype"); + } + o.attachments_to_add.back().mimetype = parameter; +} + +void +ArgParser::argAADescription(char* parameter) +{ + o.attachments_to_add.back().description = parameter; +} + +void +ArgParser::argAAReplace() +{ + o.attachments_to_add.back().replace = true; +} + +void +ArgParser::argEndAddAttachment() +{ + static std::string now = QUtil::qpdf_time_to_pdf_time( + QUtil::get_current_qpdf_time()); + this->option_table = &(this->main_option_table); + auto& cur = o.attachments_to_add.back(); + if (cur.path.empty()) + { + usage("add attachment: no path specified"); + } + std::string last_element = cur.path; + size_t pathsep = cur.path.find_last_of("/\\"); + if (pathsep != std::string::npos) + { + last_element = cur.path.substr(pathsep + 1); + if (last_element.empty()) + { + usage("path for --add-attachment may not end" + " with a path separator"); + } + } + if (cur.filename.empty()) + { + cur.filename = last_element; + } + if (cur.key.empty()) + { + cur.key = last_element; + } + if (cur.creationdate.empty()) + { + cur.creationdate = now; + } + if (cur.moddate.empty()) + { + cur.moddate = now; + } +} + +void +ArgParser::argCApositional(char* arg) +{ + o.attachments_to_copy.back().path = arg; +} + +void +ArgParser::argCAprefix(char* parameter) +{ + o.attachments_to_copy.back().prefix = parameter; +} + +void +ArgParser::argCApassword(char* parameter) +{ + o.attachments_to_copy.back().password = parameter; +} + +void +ArgParser::argEndCopyAttachments() +{ + this->option_table = &(this->main_option_table); + if (o.attachments_to_copy.back().path.empty()) + { + usage("copy attachments: no path specified"); + } +} + void ArgParser::handleArgFileArguments() { @@ -3768,6 +4062,66 @@ static void do_show_pages(QPDF& pdf, Options& o) } } +static void do_list_attachments(QPDF& pdf, Options& o) +{ + QPDFEmbeddedFileDocumentHelper efdh(pdf); + if (efdh.hasEmbeddedFiles()) + { + for (auto const& i: efdh.getEmbeddedFiles()) + { + std::string const& key = i.first; + auto efoh = i.second; + std::cout << key << " -> " + << efoh->getEmbeddedFileStream().getObjGen() + << std::endl; + if (o.verbose) + { + auto desc = efoh->getDescription(); + if (! desc.empty()) + { + std::cout << " description: " << desc << std::endl; + } + std::cout << " preferred name: " << efoh->getFilename() + << std::endl; + std::cout << " all names:" << std::endl; + for (auto const& i2: efoh->getFilenames()) + { + std::cout << " " << i2.first << " -> " << i2.second + << std::endl; + } + std::cout << " all data streams:" << std::endl; + for (auto i2: QPDFDictItems(efoh->getEmbeddedFileStreams())) + { + std::cout << " " << i2.first << " -> " + << i2.second.getObjGen() + << std::endl; + } + } + } + } + else + { + std::cout << o.infilename << " has no embedded files" << std::endl; + } +} + +static void do_show_attachment(QPDF& pdf, Options& o, int& exit_code) +{ + QPDFEmbeddedFileDocumentHelper efdh(pdf); + auto fs = efdh.getEmbeddedFile(o.attachment_to_show); + if (! fs) + { + std::cerr << whoami << ": attachment " << o.attachment_to_show + << " not found" << std::endl; + exit_code = EXIT_ERROR; + return; + } + auto efs = fs->getEmbeddedFileStream(); + QUtil::binary_stdout(); + Pl_StdioFile out("stdout", stdout); + efs.pipeStreamData(&out, 0, qpdf_dl_all); +} + static std::set get_wanted_json_objects(Options& o) { @@ -4354,6 +4708,14 @@ static void do_inspection(QPDF& pdf, Options& o) { do_show_pages(pdf, o); } + if (o.list_attachments) + { + do_list_attachments(pdf, o); + } + if (! o.attachment_to_show.empty()) + { + do_show_attachment(pdf, o, exit_code); + } if ((! pdf.getWarnings().empty()) && (exit_code != EXIT_ERROR)) { std::cerr << whoami @@ -4858,7 +5220,106 @@ static void handle_under_overlay(QPDF& pdf, Options& o) } } -static void handle_transformations(QPDF& pdf, Options& o) +static void maybe_set_pagemode(QPDF& pdf, std::string const& pagemode) +{ + auto root = pdf.getRoot(); + if (root.getKey("/PageMode").isNull()) + { + root.replaceKey("/PageMode", QPDFObjectHandle::newName(pagemode)); + } +} + +static void add_attachments(QPDF& pdf, Options& o, int& exit_code) +{ + maybe_set_pagemode(pdf, "/UseAttachments"); + QPDFEmbeddedFileDocumentHelper efdh(pdf); + for (auto const& to_add: o.attachments_to_add) + { + if ((! to_add.replace) && efdh.getEmbeddedFile(to_add.key)) + { + std::cerr << whoami << ": " << pdf.getFilename() + << " already has an attachment with key = " + << to_add.key << "; use --replace to replace" + << " or --key to specificy a different key" + << std::endl; + exit_code = EXIT_ERROR; + continue; + } + + auto fs = QPDFFileSpecObjectHelper::createFileSpec( + pdf, to_add.filename, to_add.path); + if (! to_add.description.empty()) + { + fs.setDescription(to_add.description); + } + auto efs = QPDFEFStreamObjectHelper(fs.getEmbeddedFileStream()); + efs.setCreationDate(to_add.creationdate) + .setModDate(to_add.moddate); + if (! to_add.mimetype.empty()) + { + efs.setSubtype(to_add.mimetype); + } + + efdh.replaceEmbeddedFile(to_add.key, fs); + if (o.verbose) + { + std::cout << whoami << ": attached " << to_add.path + << " as " << to_add.filename + << " with key " << to_add.key << std::endl; + } + } +} + +static void copy_attachments(QPDF& pdf, Options& o, int& exit_code) +{ + maybe_set_pagemode(pdf, "/UseAttachments"); + QPDFEmbeddedFileDocumentHelper efdh(pdf); + for (auto const& to_copy: o.attachments_to_copy) + { + auto other = process_file( + to_copy.path.c_str(), to_copy.password.c_str(), o); + QPDFEmbeddedFileDocumentHelper other_efdh(*other); + auto other_attachments = other_efdh.getEmbeddedFiles(); + for (auto const& iter: other_attachments) + { + if (o.verbose) + { + std::cout << whoami << ": copying attachments from " + << to_copy.path << std::endl; + } + std::string new_key = to_copy.prefix + iter.first; + if (efdh.getEmbeddedFile(new_key)) + { + exit_code = EXIT_ERROR; + std::cerr << whoami << to_copy.path << " and " + << pdf.getFilename() + << " both have attachments with key " << new_key + << "; use --prefix with --copy-attachments-from" + << " or manually copy individual attachments" + << std::endl; + } + else + { + auto new_fs_oh = pdf.copyForeignObject( + iter.second->getObjectHandle()); + efdh.replaceEmbeddedFile( + new_key, QPDFFileSpecObjectHelper(new_fs_oh)); + if (o.verbose) + { + std::cout << " " << iter.first << " -> " << new_key + << std::endl; + } + } + } + + if ((other->anyWarnings()) && (exit_code == 0)) + { + exit_code = EXIT_WARNING; + } + } +} + +static void handle_transformations(QPDF& pdf, Options& o, int& exit_code) { QPDFPageDocumentHelper dh(pdf); if (o.externalize_inline_images) @@ -4935,6 +5396,35 @@ static void handle_transformations(QPDF& pdf, Options& o) { pdf.getRoot().removeKey("/PageLabels"); } + if (! o.attachments_to_remove.empty()) + { + QPDFEmbeddedFileDocumentHelper efdh(pdf); + for (auto const& key: o.attachments_to_remove) + { + if (efdh.removeEmbeddedFile(key)) + { + if (o.verbose) + { + std::cout << whoami << + ": removed attachment " << key << std::endl; + } + } + else + { + std::cerr << whoami << + ": attachment " << key << " not found" << std::endl; + exit_code = EXIT_ERROR; + } + } + } + if (! o.attachments_to_add.empty()) + { + add_attachments(pdf, o, exit_code); + } + if (! o.attachments_to_copy.empty()) + { + copy_attachments(pdf, o, exit_code); + } } static bool should_remove_unreferenced_resources(QPDF& pdf, Options& o) @@ -5854,6 +6344,7 @@ int realmain(int argc, char* argv[]) Options o; ArgParser ap(argc, argv, o); + int exit_code = 0; try { ap.parseOptions(); @@ -5906,7 +6397,7 @@ int realmain(int argc, char* argv[]) handle_rotations(pdf, o); } handle_under_overlay(pdf, o); - handle_transformations(pdf, o); + handle_transformations(pdf, o, exit_code); if ((o.outfilename == 0) && (! o.replace_input)) { @@ -5929,7 +6420,10 @@ int realmain(int argc, char* argv[]) << std::endl; } // Still return with warning code even if warnings were suppressed. - return EXIT_WARNING; + if (exit_code == 0) + { + exit_code = EXIT_WARNING; + } } } catch (std::exception& e) @@ -5938,7 +6432,7 @@ int realmain(int argc, char* argv[]) return EXIT_ERROR; } - return 0; + return exit_code; } #ifdef WINDOWS_WMAIN diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 2412f6d4..348d3948 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 += 4; +$n_tests += 33; open(F, ">auto-txt") or die; print F "from file"; @@ -532,16 +532,183 @@ $td->runtest("attachments", {$td->COMMAND => "test_driver 76 minimal.pdf auto-txt"}, {$td->FILE => "test76.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("show attachment", + {$td->COMMAND => "qpdf --show-attachment=att1 a.pdf"}, + {$td->STRING => "from file", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); $td->runtest("check output", {$td->FILE => "a.pdf"}, {$td->FILE => "test76.pdf"}); -$td->runtest("attachments", +$td->runtest("list attachments", + {$td->COMMAND => "qpdf --list-attachments a.pdf"}, + {$td->FILE => "test76-list.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$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("remove attachment (test_driver)", {$td->COMMAND => "test_driver 77 test76.pdf"}, {$td->STRING => "test 77 done\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->runtest("check output", {$td->FILE => "a.pdf"}, {$td->FILE => "test77.pdf"}); +$td->runtest("remove attachment (cli)", + {$td->COMMAND => "qpdf --remove-attachment=att2 test76.pdf" . + " --static-id --qdf --verbose b.pdf"}, + {$td->FILE => "remove-attachment.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "b.pdf"}, + {$td->FILE => "test77.pdf"}); +$td->runtest("show missing attachment", + {$td->COMMAND => "qpdf --show-attachment=att2 b.pdf"}, + {$td->STRING => "qpdf: attachment att2 not found\n", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("remove missing attachment", + {$td->COMMAND => "qpdf --remove-attachment=att2 b.pdf c.pdf"}, + {$td->STRING => "qpdf: attachment att2 not found\n", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + +$td->runtest("add attachment: bad creation date", + {$td->COMMAND => "qpdf minimal.pdf a.pdf" . + " --add-attachment auto-txt --creationdate=potato --"}, + {$td->REGEXP => ".*potato is not a valid PDF timestamp.*", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachment: bad mod date", + {$td->COMMAND => "qpdf minimal.pdf a.pdf" . + " --add-attachment auto-txt --moddate=potato --"}, + {$td->REGEXP => ".*potato is not a valid PDF timestamp.*", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachment: bad mod date", + {$td->COMMAND => "qpdf minimal.pdf a.pdf" . + " --add-attachment auto-txt --mimetype=potato --"}, + {$td->REGEXP => + ".*mime type should be specified as type/subtype.*", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachment: trailing slash", + {$td->COMMAND => "qpdf minimal.pdf a.pdf" . + " --add-attachment auto-txt/ --"}, + {$td->REGEXP => ".*may not end with a path separator.*", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachment: trailing slash", + {$td->COMMAND => "qpdf minimal.pdf a.pdf" . + " --add-attachment --"}, + {$td->REGEXP => ".*add attachment: no path specified.*", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + +foreach my $i (qw(1 2 3)) +{ + open(F, ">auto-$i") or die; + print F "attachment $i"; + close(F); +} +my @dates = ("--creationdate=D:20210210091359-05'00'", + "--moddate=D:20210210141359Z"); +$td->runtest("add attachments", + {$td->COMMAND => + [qw(qpdf minimal.pdf a.pdf --no-original-object-ids), + qw(--verbose --static-id --qdf), + qw(--add-attachment ./auto-1), @dates, + qw(--mimetype=text/plain --), + qw(--add-attachment ./auto-2 --key=auto-Two), @dates, '--', + qw(--add-attachment ./auto-3 --filename=auto-Three.txt), + @dates, '--description=two words', '--']}, + {$td->FILE => "add-attachments-1.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("list attachments", + {$td->COMMAND => "qpdf --list-attachments a.pdf --verbose"}, + {$td->FILE => "list-attachments-1.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "add-attachments-1.pdf"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachments: duplicate", + {$td->COMMAND => + "qpdf a.pdf b.pdf --verbose --add-attachment ./auto-1 --"}, + {$td->FILE => "add-attachments-duplicate.out", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachments: replace", + {$td->COMMAND => + [qw(qpdf a.pdf b.pdf --no-original-object-ids), + qw(--verbose --static-id --qdf), + qw(--add-attachment ./auto-2 --key=auto-1 --replace), + @dates, '--']}, + {$td->FILE => "add-attachments-2.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("list attachments", + {$td->COMMAND => "qpdf --list-attachments b.pdf --verbose"}, + {$td->FILE => "list-attachments-3.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "b.pdf"}, + {$td->FILE => "add-attachments-2.pdf"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("copy attachments", + {$td->COMMAND => + "qpdf --verbose --no-original-object-ids" . + " --static-id --qdf minimal.pdf b.pdf" . + " --copy-attachments-from a.pdf --"}, + {$td->FILE => "copy-attachments-1.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("list attachments", + {$td->COMMAND => "qpdf --list-attachments b.pdf --verbose"}, + {$td->FILE => "list-attachments-1.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "b.pdf"}, + {$td->FILE => "add-attachments-1.pdf"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("copy attachments: duplicate", + {$td->COMMAND => + "qpdf --verbose --no-original-object-ids" . + " --static-id --qdf a.pdf c.pdf" . + " --copy-attachments-from b.pdf --"}, + {$td->FILE => "copy-attachments-duplicate.out", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("copy attachments: prefix", + {$td->COMMAND => + "qpdf --verbose --no-original-object-ids" . + " --static-id --qdf a.pdf c.pdf" . + " --copy-attachments-from b.pdf --prefix=1- --"}, + {$td->FILE => "copy-attachments-2.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("list attachments", + {$td->COMMAND => "qpdf --list-attachments c.pdf --verbose"}, + {$td->FILE => "list-attachments-2.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "c.pdf"}, + {$td->FILE => "copy-attachments-2.pdf"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("add attachments: current date", + {$td->COMMAND => + [qw(qpdf minimal.pdf a.pdf --encrypt u o 256 --), + qw(--verbose --add-attachment ./auto-1 --)]}, + {$td->FILE => "add-attachments-3.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("list attachments", + {$td->COMMAND => + "qpdf --password=u --list-attachments a.pdf --verbose"}, + {$td->FILE => "list-attachments-4.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +# The object to show here is the one in list-attachments-4.out +$td->runtest("check dates", + {$td->COMMAND => "qpdf --show-object=6 a.pdf --password=u"}, + {$td->REGEXP => ".*CreationDate \\(D:\\d+.*ModDate \\(D:\\d+.*", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); show_ntests(); # ---------- diff --git a/qpdf/qtest/qpdf/add-attachments-1.out b/qpdf/qtest/qpdf/add-attachments-1.out new file mode 100644 index 00000000..7954b314 --- /dev/null +++ b/qpdf/qtest/qpdf/add-attachments-1.out @@ -0,0 +1,4 @@ +qpdf: attached ./auto-1 as auto-1 with key auto-1 +qpdf: attached ./auto-2 as auto-2 with key auto-Two +qpdf: attached ./auto-3 as auto-Three.txt with key auto-3 +qpdf: wrote file a.pdf diff --git a/qpdf/qtest/qpdf/add-attachments-1.pdf b/qpdf/qtest/qpdf/add-attachments-1.pdf new file mode 100644 index 00000000..9605188f --- /dev/null +++ b/qpdf/qtest/qpdf/add-attachments-1.pdf @@ -0,0 +1,223 @@ +%PDF-1.3 +%¿÷¢þ +%QDF-1.0 + +1 0 obj +<< + /Names << + /EmbeddedFiles 2 0 R + >> + /PageMode /UseAttachments + /Pages 3 0 R + /Type /Catalog +>> +endobj + +2 0 obj +<< + /Names [ + (auto-1) + 4 0 R + (auto-3) + 5 0 R + (auto-Two) + 6 0 R + ] +>> +endobj + +3 0 obj +<< + /Count 1 + /Kids [ + 7 0 R + ] + /Type /Pages +>> +endobj + +4 0 obj +<< + /EF << + /F 8 0 R + /UF 8 0 R + >> + /F (auto-1) + /Type /Filespec + /UF (auto-1) +>> +endobj + +5 0 obj +<< + /Desc (two words) + /EF << + /F 10 0 R + /UF 10 0 R + >> + /F (auto-Three.txt) + /Type /Filespec + /UF (auto-Three.txt) +>> +endobj + +6 0 obj +<< + /EF << + /F 12 0 R + /UF 12 0 R + >> + /F (auto-2) + /Type /Filespec + /UF (auto-2) +>> +endobj + +%% Page 1 +7 0 obj +<< + /Contents 14 0 R + /MediaBox [ + 0 + 0 + 612 + 792 + ] + /Parent 3 0 R + /Resources << + /Font << + /F1 16 0 R + >> + /ProcSet 17 0 R + >> + /Type /Page +>> +endobj + +8 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + /Subtype /text#2fplain + >> + /Type /EmbeddedFile + /Length 9 0 R +>> +stream +attachment 1 +endstream +endobj + +%QDF: ignore_newline +9 0 obj +12 +endobj + +10 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 11 0 R +>> +stream +attachment 3 +endstream +endobj + +%QDF: ignore_newline +11 0 obj +12 +endobj + +12 0 obj +<< + /Params << + /CheckSum <9f991a5669c47a94f9350f53e3953e57> + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 13 0 R +>> +stream +attachment 2 +endstream +endobj + +%QDF: ignore_newline +13 0 obj +12 +endobj + +%% Contents for page 1 +14 0 obj +<< + /Length 15 0 R +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +15 0 obj +44 +endobj + +16 0 obj +<< + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font +>> +endobj + +17 0 obj +[ + /PDF + /Text +] +endobj + +xref +0 18 +0000000000 65535 f +0000000025 00000 n +0000000149 00000 n +0000000257 00000 n +0000000329 00000 n +0000000439 00000 n +0000000587 00000 n +0000000709 00000 n +0000000904 00000 n +0000001199 00000 n +0000001218 00000 n +0000001488 00000 n +0000001508 00000 n +0000001778 00000 n +0000001821 00000 n +0000001922 00000 n +0000001942 00000 n +0000002061 00000 n +trailer << + /Root 1 0 R + /Size 18 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] +>> +startxref +2097 +%%EOF diff --git a/qpdf/qtest/qpdf/add-attachments-2.out b/qpdf/qtest/qpdf/add-attachments-2.out new file mode 100644 index 00000000..36aa1446 --- /dev/null +++ b/qpdf/qtest/qpdf/add-attachments-2.out @@ -0,0 +1,2 @@ +qpdf: attached ./auto-2 as auto-2 with key auto-1 +qpdf: wrote file b.pdf diff --git a/qpdf/qtest/qpdf/add-attachments-2.pdf b/qpdf/qtest/qpdf/add-attachments-2.pdf new file mode 100644 index 00000000..fb660118 --- /dev/null +++ b/qpdf/qtest/qpdf/add-attachments-2.pdf @@ -0,0 +1,222 @@ +%PDF-1.3 +%¿÷¢þ +%QDF-1.0 + +1 0 obj +<< + /Names << + /EmbeddedFiles 2 0 R + >> + /PageMode /UseAttachments + /Pages 3 0 R + /Type /Catalog +>> +endobj + +2 0 obj +<< + /Names [ + (auto-1) + 4 0 R + (auto-3) + 5 0 R + (auto-Two) + 6 0 R + ] +>> +endobj + +3 0 obj +<< + /Count 1 + /Kids [ + 7 0 R + ] + /Type /Pages +>> +endobj + +4 0 obj +<< + /EF << + /F 8 0 R + /UF 8 0 R + >> + /F (auto-2) + /Type /Filespec + /UF (auto-2) +>> +endobj + +5 0 obj +<< + /Desc (two words) + /EF << + /F 10 0 R + /UF 10 0 R + >> + /F (auto-Three.txt) + /Type /Filespec + /UF (auto-Three.txt) +>> +endobj + +6 0 obj +<< + /EF << + /F 12 0 R + /UF 12 0 R + >> + /F (auto-2) + /Type /Filespec + /UF (auto-2) +>> +endobj + +%% Page 1 +7 0 obj +<< + /Contents 14 0 R + /MediaBox [ + 0 + 0 + 612 + 792 + ] + /Parent 3 0 R + /Resources << + /Font << + /F1 16 0 R + >> + /ProcSet 17 0 R + >> + /Type /Page +>> +endobj + +8 0 obj +<< + /Params << + /CheckSum <9f991a5669c47a94f9350f53e3953e57> + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 9 0 R +>> +stream +attachment 2 +endstream +endobj + +%QDF: ignore_newline +9 0 obj +12 +endobj + +10 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 11 0 R +>> +stream +attachment 3 +endstream +endobj + +%QDF: ignore_newline +11 0 obj +12 +endobj + +12 0 obj +<< + /Params << + /CheckSum <9f991a5669c47a94f9350f53e3953e57> + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 13 0 R +>> +stream +attachment 2 +endstream +endobj + +%QDF: ignore_newline +13 0 obj +12 +endobj + +%% Contents for page 1 +14 0 obj +<< + /Length 15 0 R +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +15 0 obj +44 +endobj + +16 0 obj +<< + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font +>> +endobj + +17 0 obj +[ + /PDF + /Text +] +endobj + +xref +0 18 +0000000000 65535 f +0000000025 00000 n +0000000149 00000 n +0000000257 00000 n +0000000329 00000 n +0000000439 00000 n +0000000587 00000 n +0000000709 00000 n +0000000904 00000 n +0000001172 00000 n +0000001191 00000 n +0000001461 00000 n +0000001481 00000 n +0000001751 00000 n +0000001794 00000 n +0000001895 00000 n +0000001915 00000 n +0000002034 00000 n +trailer << + /Root 1 0 R + /Size 18 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] +>> +startxref +2070 +%%EOF diff --git a/qpdf/qtest/qpdf/add-attachments-3.out b/qpdf/qtest/qpdf/add-attachments-3.out new file mode 100644 index 00000000..a55449c6 --- /dev/null +++ b/qpdf/qtest/qpdf/add-attachments-3.out @@ -0,0 +1,2 @@ +qpdf: attached ./auto-1 as auto-1 with key auto-1 +qpdf: wrote file a.pdf diff --git a/qpdf/qtest/qpdf/add-attachments-duplicate.out b/qpdf/qtest/qpdf/add-attachments-duplicate.out new file mode 100644 index 00000000..48b2f5eb --- /dev/null +++ b/qpdf/qtest/qpdf/add-attachments-duplicate.out @@ -0,0 +1,2 @@ +qpdf: a.pdf already has an attachment with key = auto-1; use --replace to replace or --key to specificy a different key +qpdf: wrote file b.pdf diff --git a/qpdf/qtest/qpdf/copy-attachments-1.out b/qpdf/qtest/qpdf/copy-attachments-1.out new file mode 100644 index 00000000..55030daf --- /dev/null +++ b/qpdf/qtest/qpdf/copy-attachments-1.out @@ -0,0 +1,7 @@ +qpdf: copying attachments from a.pdf + auto-1 -> auto-1 +qpdf: copying attachments from a.pdf + auto-3 -> auto-3 +qpdf: copying attachments from a.pdf + auto-Two -> auto-Two +qpdf: wrote file b.pdf diff --git a/qpdf/qtest/qpdf/copy-attachments-2.out b/qpdf/qtest/qpdf/copy-attachments-2.out new file mode 100644 index 00000000..08b5946c --- /dev/null +++ b/qpdf/qtest/qpdf/copy-attachments-2.out @@ -0,0 +1,7 @@ +qpdf: copying attachments from b.pdf + auto-1 -> 1-auto-1 +qpdf: copying attachments from b.pdf + auto-3 -> 1-auto-3 +qpdf: copying attachments from b.pdf + auto-Two -> 1-auto-Two +qpdf: wrote file c.pdf diff --git a/qpdf/qtest/qpdf/copy-attachments-2.pdf b/qpdf/qtest/qpdf/copy-attachments-2.pdf new file mode 100644 index 00000000..a2f29598 --- /dev/null +++ b/qpdf/qtest/qpdf/copy-attachments-2.pdf @@ -0,0 +1,339 @@ +%PDF-1.3 +%¿÷¢þ +%QDF-1.0 + +1 0 obj +<< + /Names << + /EmbeddedFiles 2 0 R + >> + /PageMode /UseAttachments + /Pages 3 0 R + /Type /Catalog +>> +endobj + +2 0 obj +<< + /Names [ + (1-auto-1) + 4 0 R + (1-auto-3) + 5 0 R + (1-auto-Two) + 6 0 R + (auto-1) + 7 0 R + (auto-3) + 8 0 R + (auto-Two) + 9 0 R + ] +>> +endobj + +3 0 obj +<< + /Count 1 + /Kids [ + 10 0 R + ] + /Type /Pages +>> +endobj + +4 0 obj +<< + /EF << + /F 11 0 R + /UF 11 0 R + >> + /F (auto-1) + /Type /Filespec + /UF (auto-1) +>> +endobj + +5 0 obj +<< + /Desc (two words) + /EF << + /F 13 0 R + /UF 13 0 R + >> + /F (auto-Three.txt) + /Type /Filespec + /UF (auto-Three.txt) +>> +endobj + +6 0 obj +<< + /EF << + /F 15 0 R + /UF 15 0 R + >> + /F (auto-2) + /Type /Filespec + /UF (auto-2) +>> +endobj + +7 0 obj +<< + /EF << + /F 17 0 R + /UF 17 0 R + >> + /F (auto-1) + /Type /Filespec + /UF (auto-1) +>> +endobj + +8 0 obj +<< + /Desc (two words) + /EF << + /F 19 0 R + /UF 19 0 R + >> + /F (auto-Three.txt) + /Type /Filespec + /UF (auto-Three.txt) +>> +endobj + +9 0 obj +<< + /EF << + /F 21 0 R + /UF 21 0 R + >> + /F (auto-2) + /Type /Filespec + /UF (auto-2) +>> +endobj + +%% Page 1 +10 0 obj +<< + /Contents 23 0 R + /MediaBox [ + 0 + 0 + 612 + 792 + ] + /Parent 3 0 R + /Resources << + /Font << + /F1 25 0 R + >> + /ProcSet 26 0 R + >> + /Type /Page +>> +endobj + +11 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + /Subtype /text#2fplain + >> + /Type /EmbeddedFile + /Length 12 0 R +>> +stream +attachment 1 +endstream +endobj + +%QDF: ignore_newline +12 0 obj +12 +endobj + +13 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 14 0 R +>> +stream +attachment 3 +endstream +endobj + +%QDF: ignore_newline +14 0 obj +12 +endobj + +15 0 obj +<< + /Params << + /CheckSum <9f991a5669c47a94f9350f53e3953e57> + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 16 0 R +>> +stream +attachment 2 +endstream +endobj + +%QDF: ignore_newline +16 0 obj +12 +endobj + +17 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + /Subtype /text#2fplain + >> + /Type /EmbeddedFile + /Length 18 0 R +>> +stream +attachment 1 +endstream +endobj + +%QDF: ignore_newline +18 0 obj +12 +endobj + +19 0 obj +<< + /Params << + /CheckSum + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 20 0 R +>> +stream +attachment 3 +endstream +endobj + +%QDF: ignore_newline +20 0 obj +12 +endobj + +21 0 obj +<< + /Params << + /CheckSum <9f991a5669c47a94f9350f53e3953e57> + /CreationDate (D:20210210091359-05'00') + /ModDate (D:20210210141359Z) + /Size 12 + >> + /Type /EmbeddedFile + /Length 22 0 R +>> +stream +attachment 2 +endstream +endobj + +%QDF: ignore_newline +22 0 obj +12 +endobj + +%% Contents for page 1 +23 0 obj +<< + /Length 24 0 R +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +24 0 obj +44 +endobj + +25 0 obj +<< + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font +>> +endobj + +26 0 obj +[ + /PDF + /Text +] +endobj + +xref +0 27 +0000000000 65535 f +0000000025 00000 n +0000000149 00000 n +0000000334 00000 n +0000000407 00000 n +0000000519 00000 n +0000000667 00000 n +0000000779 00000 n +0000000891 00000 n +0000001039 00000 n +0000001161 00000 n +0000001357 00000 n +0000001654 00000 n +0000001674 00000 n +0000001944 00000 n +0000001964 00000 n +0000002234 00000 n +0000002254 00000 n +0000002551 00000 n +0000002571 00000 n +0000002841 00000 n +0000002861 00000 n +0000003131 00000 n +0000003174 00000 n +0000003275 00000 n +0000003295 00000 n +0000003414 00000 n +trailer << + /Root 1 0 R + /Size 27 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] +>> +startxref +3450 +%%EOF diff --git a/qpdf/qtest/qpdf/copy-attachments-duplicate.out b/qpdf/qtest/qpdf/copy-attachments-duplicate.out new file mode 100644 index 00000000..302c0f9c --- /dev/null +++ b/qpdf/qtest/qpdf/copy-attachments-duplicate.out @@ -0,0 +1,7 @@ +qpdf: copying attachments from b.pdf +qpdfb.pdf and a.pdf both have attachments with key auto-1; use --prefix with --copy-attachments-from or manually copy individual attachments +qpdf: copying attachments from b.pdf +qpdfb.pdf and a.pdf both have attachments with key auto-3; use --prefix with --copy-attachments-from or manually copy individual attachments +qpdf: copying attachments from b.pdf +qpdfb.pdf and a.pdf both have attachments with key auto-Two; use --prefix with --copy-attachments-from or manually copy individual attachments +qpdf: wrote file c.pdf diff --git a/qpdf/qtest/qpdf/list-attachments-1.out b/qpdf/qtest/qpdf/list-attachments-1.out new file mode 100644 index 00000000..6e94cc64 --- /dev/null +++ b/qpdf/qtest/qpdf/list-attachments-1.out @@ -0,0 +1,25 @@ +auto-1 -> 8,0 + preferred name: auto-1 + all names: + /F -> auto-1 + /UF -> auto-1 + all data streams: + /F -> 8,0 + /UF -> 8,0 +auto-3 -> 10,0 + description: two words + preferred name: auto-Three.txt + all names: + /F -> auto-Three.txt + /UF -> auto-Three.txt + all data streams: + /F -> 10,0 + /UF -> 10,0 +auto-Two -> 12,0 + preferred name: auto-2 + all names: + /F -> auto-2 + /UF -> auto-2 + all data streams: + /F -> 12,0 + /UF -> 12,0 diff --git a/qpdf/qtest/qpdf/list-attachments-2.out b/qpdf/qtest/qpdf/list-attachments-2.out new file mode 100644 index 00000000..d5488020 --- /dev/null +++ b/qpdf/qtest/qpdf/list-attachments-2.out @@ -0,0 +1,50 @@ +1-auto-1 -> 11,0 + preferred name: auto-1 + all names: + /F -> auto-1 + /UF -> auto-1 + all data streams: + /F -> 11,0 + /UF -> 11,0 +1-auto-3 -> 13,0 + description: two words + preferred name: auto-Three.txt + all names: + /F -> auto-Three.txt + /UF -> auto-Three.txt + all data streams: + /F -> 13,0 + /UF -> 13,0 +1-auto-Two -> 15,0 + preferred name: auto-2 + all names: + /F -> auto-2 + /UF -> auto-2 + all data streams: + /F -> 15,0 + /UF -> 15,0 +auto-1 -> 17,0 + preferred name: auto-1 + all names: + /F -> auto-1 + /UF -> auto-1 + all data streams: + /F -> 17,0 + /UF -> 17,0 +auto-3 -> 19,0 + description: two words + preferred name: auto-Three.txt + all names: + /F -> auto-Three.txt + /UF -> auto-Three.txt + all data streams: + /F -> 19,0 + /UF -> 19,0 +auto-Two -> 21,0 + preferred name: auto-2 + all names: + /F -> auto-2 + /UF -> auto-2 + all data streams: + /F -> 21,0 + /UF -> 21,0 diff --git a/qpdf/qtest/qpdf/list-attachments-3.out b/qpdf/qtest/qpdf/list-attachments-3.out new file mode 100644 index 00000000..0467b59d --- /dev/null +++ b/qpdf/qtest/qpdf/list-attachments-3.out @@ -0,0 +1,25 @@ +auto-1 -> 8,0 + preferred name: auto-2 + all names: + /F -> auto-2 + /UF -> auto-2 + all data streams: + /F -> 8,0 + /UF -> 8,0 +auto-3 -> 10,0 + description: two words + preferred name: auto-Three.txt + all names: + /F -> auto-Three.txt + /UF -> auto-Three.txt + all data streams: + /F -> 10,0 + /UF -> 10,0 +auto-Two -> 12,0 + preferred name: auto-2 + all names: + /F -> auto-2 + /UF -> auto-2 + all data streams: + /F -> 12,0 + /UF -> 12,0 diff --git a/qpdf/qtest/qpdf/list-attachments-4.out b/qpdf/qtest/qpdf/list-attachments-4.out new file mode 100644 index 00000000..b2d59e08 --- /dev/null +++ b/qpdf/qtest/qpdf/list-attachments-4.out @@ -0,0 +1,8 @@ +auto-1 -> 6,0 + preferred name: auto-1 + all names: + /F -> auto-1 + /UF -> auto-1 + all data streams: + /F -> 6,0 + /UF -> 6,0 diff --git a/qpdf/qtest/qpdf/remove-attachment.out b/qpdf/qtest/qpdf/remove-attachment.out new file mode 100644 index 00000000..d38bbbad --- /dev/null +++ b/qpdf/qtest/qpdf/remove-attachment.out @@ -0,0 +1,2 @@ +qpdf: removed attachment att2 +qpdf: wrote file b.pdf diff --git a/qpdf/qtest/qpdf/test76-list-verbose.out b/qpdf/qtest/qpdf/test76-list-verbose.out new file mode 100644 index 00000000..5e6df1a2 --- /dev/null +++ b/qpdf/qtest/qpdf/test76-list-verbose.out @@ -0,0 +1,25 @@ +att1 -> 8,0 + description: some text + preferred name: att1.txt + all names: + /F -> att1.txt + /UF -> att1.txt + all data streams: + /F -> 8,0 + /UF -> 8,0 +att2 -> 10,0 + preferred name: att2.txt + all names: + /F -> att2.txt + /UF -> att2.txt + all data streams: + /F -> 10,0 + /UF -> 10,0 +att3 -> 12,0 + preferred name: Ï€.txt + all names: + /F -> att3.txt + /UF -> Ï€.txt + all data streams: + /F -> 12,0 + /UF -> 12,0 diff --git a/qpdf/qtest/qpdf/test76-list.out b/qpdf/qtest/qpdf/test76-list.out new file mode 100644 index 00000000..38cb2502 --- /dev/null +++ b/qpdf/qtest/qpdf/test76-list.out @@ -0,0 +1,3 @@ +att1 -> 8,0 +att2 -> 10,0 +att3 -> 12,0