aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2021-02-09 21:30:56 +0100
committerJay Berkenbilt <ejb@ql.org>2021-02-10 16:03:27 +0100
commit832d792e4e88b85f4926e1241870de4d6ec2d772 (patch)
tree71c94ad1878bd3071b308230501a24aa5a629509
parent1f4771cd0d1ff53a5d171606b6659324597d9e14 (diff)
downloadqpdf-832d792e4e88b85f4926e1241870de4d6ec2d772.tar.zst
Add CLI support for working with attachments
-rw-r--r--ChangeLog6
-rw-r--r--manual/qpdf-manual.xml182
-rw-r--r--qpdf/qpdf.cc504
-rw-r--r--qpdf/qtest/qpdf.test171
-rw-r--r--qpdf/qtest/qpdf/add-attachments-1.out4
-rw-r--r--qpdf/qtest/qpdf/add-attachments-1.pdf223
-rw-r--r--qpdf/qtest/qpdf/add-attachments-2.out2
-rw-r--r--qpdf/qtest/qpdf/add-attachments-2.pdf222
-rw-r--r--qpdf/qtest/qpdf/add-attachments-3.out2
-rw-r--r--qpdf/qtest/qpdf/add-attachments-duplicate.out2
-rw-r--r--qpdf/qtest/qpdf/copy-attachments-1.out7
-rw-r--r--qpdf/qtest/qpdf/copy-attachments-2.out7
-rw-r--r--qpdf/qtest/qpdf/copy-attachments-2.pdf339
-rw-r--r--qpdf/qtest/qpdf/copy-attachments-duplicate.out7
-rw-r--r--qpdf/qtest/qpdf/list-attachments-1.out25
-rw-r--r--qpdf/qtest/qpdf/list-attachments-2.out50
-rw-r--r--qpdf/qtest/qpdf/list-attachments-3.out25
-rw-r--r--qpdf/qtest/qpdf/list-attachments-4.out8
-rw-r--r--qpdf/qtest/qpdf/remove-attachment.out2
-rw-r--r--qpdf/qtest/qpdf/test76-list-verbose.out25
-rw-r--r--qpdf/qtest/qpdf/test76-list.out3
21 files changed, 1809 insertions, 7 deletions
diff --git a/ChangeLog b/ChangeLog
index 0511ca53..be8d2564 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+2021-02-10 Jay Berkenbilt <ejb@ql.org>
+
+ * 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 <ejb@ql.org>
* 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</option>
</itemizedlist>
</para>
</sect1>
+ <sect1 id="ref.attachments">
+ <title>Embedded Files/Attachments Options</title>
+ <para>
+ Starting with qpdf 10.2, you can work with file attachments in PDF
+ files from the command line. The following options are available:
+ <variablelist>
+ <varlistentry>
+ <term><option>--list-attachments</option></term>
+ <listitem>
+ <para>
+ Show the &ldquo;key&rdquo; and stream number for embedded
+ files. With <option>--verbose</option>, 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.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--show-attachment=<replaceable>key</replaceable></option></term>
+ <listitem>
+ <para>
+ Write the contents of the specified attachment to standard
+ output as binary data. The key should match one of the keys
+ shown by <option>--list-attachments</option>. If specified
+ multiple times, only the last attachment will be shown.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--add-attachment <replaceable>file</replaceable> <replaceable>options</replaceable> --</option></term>
+ <listitem>
+ <para>
+ Add or replace an attachment with the contents of
+ <replaceable>file</replaceable>. This may be specified more
+ than once. The following additional options may appear before
+ the <literal>--</literal> that ends this option:
+ <variablelist>
+ <varlistentry>
+ <term><option>--key=<replaceable>key</replaceable></option></term>
+ <listitem>
+ <para>
+ The key to use to register the attachment in the embedded
+ files table. Defaults to the last path element of
+ <replaceable>file</replaceable>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--filename=<replaceable>name</replaceable></option></term>
+ <listitem>
+ <para>
+ 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 <replaceable>file</replaceable>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--creationdate=<replaceable>date</replaceable></option></term>
+ <listitem>
+ <para>
+ The attachment's creation date in PDF format; defaults to
+ the current time. The date format is explained below.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--moddate=<replaceable>date</replaceable></option></term>
+ <listitem>
+ <para>
+ The attachment's modification date in PDF format; defaults
+ to the current time. The date format is explained below.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--mimetype=<replaceable>type/subtype</replaceable></option></term>
+ <listitem>
+ <para>
+ The mime type for the attachment, e.g.
+ <literal>text/plain</literal> or
+ <literal>application/pdf</literal>. Note that the mimetype
+ appears in a field called <literal>/Subtype</literal> in
+ the PDF but actually includes the full type and subtype of
+ the mime type.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--description=<replaceable>&quot;text&quot;</replaceable></option></term>
+ <listitem>
+ <para>
+ Descriptive text for the attachment, displayed by some PDF
+ viewers.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--replace</option></term>
+ <listitem>
+ <para>
+ Indicates that any existing attachment with the same key
+ should be replaced by the new attachment. Otherwise,
+ <command>qpdf</command> gives an error if an attachment
+ with that key is already present.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--remove-attachment=<replaceable>key</replaceable></option></term>
+ <listitem>
+ <para>
+ 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 <option>--verbose</option>
+ to see status of the removal.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--copy-attachments-from <replaceable>file</replaceable> <replaceable>options</replaceable> --</option></term>
+ <listitem>
+ <para>
+ Copy attachments from another file. This may be specified more
+ than once. The following additional options may appear before
+ the <literal>--</literal> that ends this option:
+ <variablelist>
+ <varlistentry>
+ <term><option>--password=<replaceable>password</replaceable></option></term>
+ <listitem>
+ <para>
+ If required, the password needed to open
+ <replaceable>file</replaceable>
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--prefix=<replaceable>prefix</replaceable></option></term>
+ <listitem>
+ <para>
+ 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.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ When a date is required, the date should conform to the PDF date
+ format specification, which is
+ <literal>D:</literal><replaceable>yyyymmddhhmmss&lt;z&gt;</replaceable>,
+ where <replaceable>&lt;z&gt;</replaceable> is either
+ <literal>Z</literal> for UTC or a timezone offset in the form
+ <replaceable>-hh'mm'</replaceable> or
+ <replaceable>+hh'mm'</replaceable>. Examples:
+ <literal>D:20210207161528-05'00'</literal>,
+ <literal>D:20210207211528Z</literal>.
+ </para>
+ </sect1>
<sect1 id="ref.advanced-parsing">
<title>Advanced Parsing Options</title>
<para>
@@ -4913,6 +5088,13 @@ print "\n";
<itemizedlist>
<listitem>
<para>
+ Add new command line options for listing, saving, adding,
+ removing, and and copying file attachments. See <xref
+ linkend="ref.attachments"/> for details.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
The option
<option>--password-file=<replaceable>filename</replaceable></option>
can now be used to read the decryption password from a file.
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 <qpdf/QPDFExc.hh>
#include <qpdf/QPDFSystemError.hh>
#include <qpdf/QPDFCryptoProvider.hh>
+#include <qpdf/QPDFEmbeddedFileDocumentHelper.hh>
#include <qpdf/QPDFWriter.hh>
#include <qpdf/QIntC.hh>
@@ -95,6 +96,31 @@ struct UnderOverlay
std::vector<int> 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<std::string> attachments_to_remove;
+ std::list<AddAttachment> attachments_to_add;
+ std::list<CopyAttachmentFrom> attachments_to_copy;
bool json;
std::set<std::string> json_keys;
std::set<std::string> 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<std::string, OptionEntry> encrypt128_option_table;
std::map<std::string, OptionEntry> encrypt256_option_table;
std::map<std::string, OptionEntry> under_overlay_option_table;
+ std::map<std::string, OptionEntry> add_attachment_option_table;
+ std::map<std::string, OptionEntry> copy_attachments_option_table;
std::vector<PointerHolder<char> > new_argv;
std::vector<PointerHolder<char> > bash_argv;
PointerHolder<char*> 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<z> where <z> 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"
@@ -1961,6 +2093,40 @@ ArgParser::argFlattenRotation()
}
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)
{
o.stream_data_set = true;
@@ -2618,6 +2784,134 @@ ArgParser::argRequiresPassword()
}
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()
{
// Support reading arguments from files. Create a new argv. Ensure
@@ -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<QPDFObjGen>
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 <a857d18d3fc23ad412122ef040733331>
+ /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 <d6c7ac7cf295ae133fea186cfd068dab>
+ /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 <d6c7ac7cf295ae133fea186cfd068dab>
+ /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 <a857d18d3fc23ad412122ef040733331>
+ /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 <d6c7ac7cf295ae133fea186cfd068dab>
+ /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 <a857d18d3fc23ad412122ef040733331>
+ /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 <d6c7ac7cf295ae133fea186cfd068dab>
+ /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