Commit 832d792e4e88b85f4926e1241870de4d6ec2d772

Authored by Jay Berkenbilt
1 parent 1f4771cd

Add CLI support for working with attachments

ChangeLog
  1 +2021-02-10 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Add new command-line arguments for operating on attachments:
  4 + --list-attachments, --add-attachment, --remove-attachment,
  5 + --copy-attachments-from. See --help and manual for details.
  6 +
1 7 2021-02-09 Jay Berkenbilt <ejb@ql.org>
2 8  
3 9 * Add methods to QUtil for working with PDF timestamp strings:
... ...
manual/qpdf-manual.xml
... ... @@ -1801,6 +1801,181 @@ outfile.pdf&lt;/option&gt;
1801 1801 </itemizedlist>
1802 1802 </para>
1803 1803 </sect1>
  1804 + <sect1 id="ref.attachments">
  1805 + <title>Embedded Files/Attachments Options</title>
  1806 + <para>
  1807 + Starting with qpdf 10.2, you can work with file attachments in PDF
  1808 + files from the command line. The following options are available:
  1809 + <variablelist>
  1810 + <varlistentry>
  1811 + <term><option>--list-attachments</option></term>
  1812 + <listitem>
  1813 + <para>
  1814 + Show the &ldquo;key&rdquo; and stream number for embedded
  1815 + files. With <option>--verbose</option>, additional
  1816 + information, including preferred file name, description,
  1817 + dates, and more are also displayed. The key is usually but not
  1818 + always equal to the file name, and is needed by some of the
  1819 + other options.
  1820 + </para>
  1821 + </listitem>
  1822 + </varlistentry>
  1823 + <varlistentry>
  1824 + <term><option>--show-attachment=<replaceable>key</replaceable></option></term>
  1825 + <listitem>
  1826 + <para>
  1827 + Write the contents of the specified attachment to standard
  1828 + output as binary data. The key should match one of the keys
  1829 + shown by <option>--list-attachments</option>. If specified
  1830 + multiple times, only the last attachment will be shown.
  1831 + </para>
  1832 + </listitem>
  1833 + </varlistentry>
  1834 + <varlistentry>
  1835 + <term><option>--add-attachment <replaceable>file</replaceable> <replaceable>options</replaceable> --</option></term>
  1836 + <listitem>
  1837 + <para>
  1838 + Add or replace an attachment with the contents of
  1839 + <replaceable>file</replaceable>. This may be specified more
  1840 + than once. The following additional options may appear before
  1841 + the <literal>--</literal> that ends this option:
  1842 + <variablelist>
  1843 + <varlistentry>
  1844 + <term><option>--key=<replaceable>key</replaceable></option></term>
  1845 + <listitem>
  1846 + <para>
  1847 + The key to use to register the attachment in the embedded
  1848 + files table. Defaults to the last path element of
  1849 + <replaceable>file</replaceable>.
  1850 + </para>
  1851 + </listitem>
  1852 + </varlistentry>
  1853 + <varlistentry>
  1854 + <term><option>--filename=<replaceable>name</replaceable></option></term>
  1855 + <listitem>
  1856 + <para>
  1857 + The file name to be used for the attachment. This is what is usually
  1858 + displayed to the user and is the name most graphical PDF
  1859 + viewers will use when saving a file. It defaults to the
  1860 + last path element of <replaceable>file</replaceable>.
  1861 + </para>
  1862 + </listitem>
  1863 + </varlistentry>
  1864 + <varlistentry>
  1865 + <term><option>--creationdate=<replaceable>date</replaceable></option></term>
  1866 + <listitem>
  1867 + <para>
  1868 + The attachment's creation date in PDF format; defaults to
  1869 + the current time. The date format is explained below.
  1870 + </para>
  1871 + </listitem>
  1872 + </varlistentry>
  1873 + <varlistentry>
  1874 + <term><option>--moddate=<replaceable>date</replaceable></option></term>
  1875 + <listitem>
  1876 + <para>
  1877 + The attachment's modification date in PDF format; defaults
  1878 + to the current time. The date format is explained below.
  1879 + </para>
  1880 + </listitem>
  1881 + </varlistentry>
  1882 + <varlistentry>
  1883 + <term><option>--mimetype=<replaceable>type/subtype</replaceable></option></term>
  1884 + <listitem>
  1885 + <para>
  1886 + The mime type for the attachment, e.g.
  1887 + <literal>text/plain</literal> or
  1888 + <literal>application/pdf</literal>. Note that the mimetype
  1889 + appears in a field called <literal>/Subtype</literal> in
  1890 + the PDF but actually includes the full type and subtype of
  1891 + the mime type.
  1892 + </para>
  1893 + </listitem>
  1894 + </varlistentry>
  1895 + <varlistentry>
  1896 + <term><option>--description=<replaceable>&quot;text&quot;</replaceable></option></term>
  1897 + <listitem>
  1898 + <para>
  1899 + Descriptive text for the attachment, displayed by some PDF
  1900 + viewers.
  1901 + </para>
  1902 + </listitem>
  1903 + </varlistentry>
  1904 + <varlistentry>
  1905 + <term><option>--replace</option></term>
  1906 + <listitem>
  1907 + <para>
  1908 + Indicates that any existing attachment with the same key
  1909 + should be replaced by the new attachment. Otherwise,
  1910 + <command>qpdf</command> gives an error if an attachment
  1911 + with that key is already present.
  1912 + </para>
  1913 + </listitem>
  1914 + </varlistentry>
  1915 + </variablelist>
  1916 + </para>
  1917 + </listitem>
  1918 + </varlistentry>
  1919 + <varlistentry>
  1920 + <term><option>--remove-attachment=<replaceable>key</replaceable></option></term>
  1921 + <listitem>
  1922 + <para>
  1923 + Remove the specified attachment. This doesn't only remove the
  1924 + attachment from the embedded files table but also clears out
  1925 + the file specification. That means that any potential internal
  1926 + links to the attachment will be broken. This option may be
  1927 + specified multiple times. Run with <option>--verbose</option>
  1928 + to see status of the removal.
  1929 + </para>
  1930 + </listitem>
  1931 + </varlistentry>
  1932 + <varlistentry>
  1933 + <term><option>--copy-attachments-from <replaceable>file</replaceable> <replaceable>options</replaceable> --</option></term>
  1934 + <listitem>
  1935 + <para>
  1936 + Copy attachments from another file. This may be specified more
  1937 + than once. The following additional options may appear before
  1938 + the <literal>--</literal> that ends this option:
  1939 + <variablelist>
  1940 + <varlistentry>
  1941 + <term><option>--password=<replaceable>password</replaceable></option></term>
  1942 + <listitem>
  1943 + <para>
  1944 + If required, the password needed to open
  1945 + <replaceable>file</replaceable>
  1946 + </para>
  1947 + </listitem>
  1948 + </varlistentry>
  1949 + <varlistentry>
  1950 + <term><option>--prefix=<replaceable>prefix</replaceable></option></term>
  1951 + <listitem>
  1952 + <para>
  1953 + Only required if the file from which attachments are being
  1954 + copied has attachments with keys that conflict with
  1955 + attachments already in the file. In this case, the
  1956 + specified prefix will be prepended to each key. This
  1957 + affects only the key in the embedded files table, not the
  1958 + file name. The PDF specification doesn't preclude multiple
  1959 + attachments having the same file name.
  1960 + </para>
  1961 + </listitem>
  1962 + </varlistentry>
  1963 + </variablelist>
  1964 + </para>
  1965 + </listitem>
  1966 + </varlistentry>
  1967 + </variablelist>
  1968 + When a date is required, the date should conform to the PDF date
  1969 + format specification, which is
  1970 + <literal>D:</literal><replaceable>yyyymmddhhmmss&lt;z&gt;</replaceable>,
  1971 + where <replaceable>&lt;z&gt;</replaceable> is either
  1972 + <literal>Z</literal> for UTC or a timezone offset in the form
  1973 + <replaceable>-hh'mm'</replaceable> or
  1974 + <replaceable>+hh'mm'</replaceable>. Examples:
  1975 + <literal>D:20210207161528-05'00'</literal>,
  1976 + <literal>D:20210207211528Z</literal>.
  1977 + </para>
  1978 + </sect1>
1804 1979 <sect1 id="ref.advanced-parsing">
1805 1980 <title>Advanced Parsing Options</title>
1806 1981 <para>
... ... @@ -4913,6 +5088,13 @@ print &quot;\n&quot;;
4913 5088 <itemizedlist>
4914 5089 <listitem>
4915 5090 <para>
  5091 + Add new command line options for listing, saving, adding,
  5092 + removing, and and copying file attachments. See <xref
  5093 + linkend="ref.attachments"/> for details.
  5094 + </para>
  5095 + </listitem>
  5096 + <listitem>
  5097 + <para>
4916 5098 The option
4917 5099 <option>--password-file=<replaceable>filename</replaceable></option>
4918 5100 can now be used to read the decryption password from a file.
... ...
qpdf/qpdf.cc
... ... @@ -26,6 +26,7 @@
26 26 #include <qpdf/QPDFExc.hh>
27 27 #include <qpdf/QPDFSystemError.hh>
28 28 #include <qpdf/QPDFCryptoProvider.hh>
  29 +#include <qpdf/QPDFEmbeddedFileDocumentHelper.hh>
29 30  
30 31 #include <qpdf/QPDFWriter.hh>
31 32 #include <qpdf/QIntC.hh>
... ... @@ -95,6 +96,31 @@ struct UnderOverlay
95 96 std::vector<int> repeat_pagenos;
96 97 };
97 98  
  99 +struct AddAttachment
  100 +{
  101 + AddAttachment() :
  102 + replace(false)
  103 + {
  104 + }
  105 +
  106 + std::string path;
  107 + std::string key;
  108 + std::string filename;
  109 + std::string creationdate;
  110 + std::string moddate;
  111 + std::string mimetype;
  112 + std::string description;
  113 + bool replace;
  114 +};
  115 +
  116 +struct CopyAttachmentFrom
  117 +{
  118 + std::string path;
  119 + std::string password;
  120 + std::string prefix;
  121 +};
  122 +
  123 +
98 124 enum remove_unref_e { re_auto, re_yes, re_no };
99 125  
100 126 struct Options
... ... @@ -177,6 +203,7 @@ struct Options
177 203 show_page_images(false),
178 204 collate(false),
179 205 flatten_rotation(false),
  206 + list_attachments(false),
180 207 json(false),
181 208 check(false),
182 209 optimize_images(false),
... ... @@ -282,6 +309,11 @@ struct Options
282 309 bool show_page_images;
283 310 bool collate;
284 311 bool flatten_rotation;
  312 + bool list_attachments;
  313 + std::string attachment_to_show;
  314 + std::list<std::string> attachments_to_remove;
  315 + std::list<AddAttachment> attachments_to_add;
  316 + std::list<CopyAttachmentFrom> attachments_to_copy;
285 317 bool json;
286 318 std::set<std::string> json_keys;
287 319 std::set<std::string> json_objects;
... ... @@ -758,6 +790,11 @@ class ArgParser
758 790 void argRotate(char* parameter);
759 791 void argCollate();
760 792 void argFlattenRotation();
  793 + void argListAttachments();
  794 + void argShowAttachment(char* parameter);
  795 + void argRemoveAttachment(char* parameter);
  796 + void argAddAttachment();
  797 + void argCopyAttachments();
761 798 void argStreamData(char* parameter);
762 799 void argCompressStreams(char* parameter);
763 800 void argRecompressFlate();
... ... @@ -838,6 +875,19 @@ class ArgParser
838 875 void argReplaceInput();
839 876 void argIsEncrypted();
840 877 void argRequiresPassword();
  878 + void argAApositional(char* arg);
  879 + void argAAKey(char* parameter);
  880 + void argAAFilename(char* parameter);
  881 + void argAACreationDate(char* parameter);
  882 + void argAAModDate(char* parameter);
  883 + void argAAMimeType(char* parameter);
  884 + void argAADescription(char* parameter);
  885 + void argAAReplace();
  886 + void argEndAddAttachment();
  887 + void argCApositional(char* arg);
  888 + void argCAprefix(char* parameter);
  889 + void argCApassword(char* parameter);
  890 + void argEndCopyAttachments();
841 891  
842 892 void usage(std::string const& message);
843 893 void checkCompletion();
... ... @@ -874,6 +924,8 @@ class ArgParser
874 924 std::map<std::string, OptionEntry> encrypt128_option_table;
875 925 std::map<std::string, OptionEntry> encrypt256_option_table;
876 926 std::map<std::string, OptionEntry> under_overlay_option_table;
  927 + std::map<std::string, OptionEntry> add_attachment_option_table;
  928 + std::map<std::string, OptionEntry> copy_attachments_option_table;
877 929 std::vector<PointerHolder<char> > new_argv;
878 930 std::vector<PointerHolder<char> > bash_argv;
879 931 PointerHolder<char*> argv_ph;
... ... @@ -982,6 +1034,13 @@ ArgParser::initOptionTable()
982 1034 {"compress", "preserve", "uncompress", 0};
983 1035 (*t)["collate"] = oe_bare(&ArgParser::argCollate);
984 1036 (*t)["flatten-rotation"] = oe_bare(&ArgParser::argFlattenRotation);
  1037 + (*t)["list-attachments"] = oe_bare(&ArgParser::argListAttachments);
  1038 + (*t)["show-attachment"] = oe_requiredParameter(
  1039 + &ArgParser::argShowAttachment, "attachment-key");
  1040 + (*t)["remove-attachment"] = oe_requiredParameter(
  1041 + &ArgParser::argRemoveAttachment, "attachment-key");
  1042 + (*t)["add-attachment"] = oe_bare(&ArgParser::argAddAttachment);
  1043 + (*t)["copy-attachments-from"] = oe_bare(&ArgParser::argCopyAttachments);
985 1044 (*t)["stream-data"] = oe_requiredChoices(
986 1045 &ArgParser::argStreamData, stream_data_choices);
987 1046 (*t)["compress-streams"] = oe_requiredChoices(
... ... @@ -1129,6 +1188,31 @@ ArgParser::initOptionTable()
1129 1188 (*t)["password"] = oe_requiredParameter(
1130 1189 &ArgParser::argUOpassword, "password");
1131 1190 (*t)["--"] = oe_bare(&ArgParser::argEndUnderOverlay);
  1191 +
  1192 + t = &this->add_attachment_option_table;
  1193 + (*t)[""] = oe_positional(&ArgParser::argAApositional);
  1194 + (*t)["key"] = oe_requiredParameter(
  1195 + &ArgParser::argAAKey, "attachment-key");
  1196 + (*t)["filename"] = oe_requiredParameter(
  1197 + &ArgParser::argAAFilename, "filename");
  1198 + (*t)["creationdate"] = oe_requiredParameter(
  1199 + &ArgParser::argAACreationDate, "creation-date");
  1200 + (*t)["moddate"] = oe_requiredParameter(
  1201 + &ArgParser::argAAModDate, "modification-date");
  1202 + (*t)["mimetype"] = oe_requiredParameter(
  1203 + &ArgParser::argAAMimeType, "mime/type");
  1204 + (*t)["description"] = oe_requiredParameter(
  1205 + &ArgParser::argAADescription, "description");
  1206 + (*t)["replace"] = oe_bare(&ArgParser::argAAReplace);
  1207 + (*t)["--"] = oe_bare(&ArgParser::argEndAddAttachment);
  1208 +
  1209 + t = &this->copy_attachments_option_table;
  1210 + (*t)[""] = oe_positional(&ArgParser::argCApositional);
  1211 + (*t)["prefix"] = oe_requiredParameter(
  1212 + &ArgParser::argCAprefix, "prefix");
  1213 + (*t)["password"] = oe_requiredParameter(
  1214 + &ArgParser::argCApassword, "password");
  1215 + (*t)["--"] = oe_bare(&ArgParser::argEndCopyAttachments);
1132 1216 }
1133 1217  
1134 1218 void
... ... @@ -1361,7 +1445,6 @@ ArgParser::argHelp()
1361 1445 << " --allow-insecure allow the owner password to be empty when the\n"
1362 1446 << " user password is not empty\n"
1363 1447 << "\n"
1364   - << "\n"
1365 1448 << " print-opt may be:\n"
1366 1449 << "\n"
1367 1450 << " full allow full printing\n"
... ... @@ -1487,6 +1570,55 @@ ArgParser::argHelp()
1487 1570 << " any \"from\" pages have been exhausted\n"
1488 1571 << "\n"
1489 1572 << "\n"
  1573 + << "Embedded Files/Attachments Options\n"
  1574 + << "----------------------------------\n"
  1575 + << "\n"
  1576 + << "These options can be used to work with embedded files, also known as\n"
  1577 + << "attachments.\n"
  1578 + << "\n"
  1579 + << "--list-attachments show key and stream number for embedded files;\n"
  1580 + << " combine with --verbose for more detailed information\n"
  1581 + << "--show-attachment=key write the contents of the specified attachment to\n"
  1582 + << " standard output as binary data\n"
  1583 + << "--add-attachment file options --\n"
  1584 + << " add or replace an attachment\n"
  1585 + << "--remove-attachment=key remove the specified attachment; repeatable\n"
  1586 + << "--copy-attachments-from file options --\n"
  1587 + << " copy attachments from another file\n"
  1588 + << "\n"
  1589 + << "The \"key\" option is the unique name under which the attachment is registered\n"
  1590 + << "within the PDF file. You can get this using the --list-attachments option. This\n"
  1591 + << "is usually the same as the filename, but it doesn't have to be.\n"
  1592 + << "\n"
  1593 + << "Options for adding attachments:\n"
  1594 + << "\n"
  1595 + << " file path to the file to attach\n"
  1596 + << " --key=key the name of this in the embedded files table;\n"
  1597 + << " defaults to the last path element of file\n"
  1598 + << " --filename=name the file name of the attachment; this is what is\n"
  1599 + << " usually displayed to the user; defaults to the\n"
  1600 + << " last path element of file\n"
  1601 + << " --creationdate=date creation date in PDF format; defaults to the\n"
  1602 + << " current time\n"
  1603 + << " --moddate=date modification date in PDF format; defaults to the\n"
  1604 + << " current time\n"
  1605 + << " --mimetype=type/subtype mime type of attachment (e.g. application/pdf)\n"
  1606 + << " --description=\"text\" attachment description\n"
  1607 + << " --replace replace any existing attachment with the same key\n"
  1608 + << "\n"
  1609 + << "Options for copying attachments:\n"
  1610 + << "\n"
  1611 + << " file file whose attachments should be copied\n"
  1612 + << " --password=password password to open the other file, if needed\n"
  1613 + << " --prefix=prefix a prefix to insert in front of each key;\n"
  1614 + << " required if needed to ensure each attachment\n"
  1615 + << " has a unique key\n"
  1616 + << "\n"
  1617 + << "Date format: D:yyyymmddhhmmss<z> where <z> is either Z for UTC or a timezone\n"
  1618 + << "offset in the form -hh'mm' or +hh'mm'.\n"
  1619 + << "Examples: D:20210207161528-05'00', D:20210207211528Z\n"
  1620 + << "\n"
  1621 + << "\n"
1490 1622 << "Advanced Parsing Options\n"
1491 1623 << "------------------------\n"
1492 1624 << "\n"
... ... @@ -1961,6 +2093,40 @@ ArgParser::argFlattenRotation()
1961 2093 }
1962 2094  
1963 2095 void
  2096 +ArgParser::argListAttachments()
  2097 +{
  2098 + o.list_attachments = true;
  2099 + o.require_outfile = false;
  2100 +}
  2101 +
  2102 +void
  2103 +ArgParser::argShowAttachment(char* parameter)
  2104 +{
  2105 + o.attachment_to_show = parameter;
  2106 + o.require_outfile = false;
  2107 +}
  2108 +
  2109 +void
  2110 +ArgParser::argRemoveAttachment(char* parameter)
  2111 +{
  2112 + o.attachments_to_remove.push_back(parameter);
  2113 +}
  2114 +
  2115 +void
  2116 +ArgParser::argAddAttachment()
  2117 +{
  2118 + this->option_table = &(this->add_attachment_option_table);
  2119 + o.attachments_to_add.push_back(AddAttachment());
  2120 +}
  2121 +
  2122 +void
  2123 +ArgParser::argCopyAttachments()
  2124 +{
  2125 + this->option_table = &(this->copy_attachments_option_table);
  2126 + o.attachments_to_copy.push_back(CopyAttachmentFrom());
  2127 +}
  2128 +
  2129 +void
1964 2130 ArgParser::argStreamData(char* parameter)
1965 2131 {
1966 2132 o.stream_data_set = true;
... ... @@ -2618,6 +2784,134 @@ ArgParser::argRequiresPassword()
2618 2784 }
2619 2785  
2620 2786 void
  2787 +ArgParser::argAApositional(char* arg)
  2788 +{
  2789 + o.attachments_to_add.back().path = arg;
  2790 +}
  2791 +
  2792 +void
  2793 +ArgParser::argAAKey(char* parameter)
  2794 +{
  2795 + o.attachments_to_add.back().key = parameter;
  2796 +}
  2797 +
  2798 +void
  2799 +ArgParser::argAAFilename(char* parameter)
  2800 +{
  2801 + o.attachments_to_add.back().filename = parameter;
  2802 +}
  2803 +
  2804 +void
  2805 +ArgParser::argAACreationDate(char* parameter)
  2806 +{
  2807 + if (! QUtil::pdf_time_to_qpdf_time(parameter))
  2808 + {
  2809 + usage(std::string(parameter) + " is not a valid PDF timestamp");
  2810 + }
  2811 + o.attachments_to_add.back().creationdate = parameter;
  2812 +}
  2813 +
  2814 +void
  2815 +ArgParser::argAAModDate(char* parameter)
  2816 +{
  2817 + if (! QUtil::pdf_time_to_qpdf_time(parameter))
  2818 + {
  2819 + usage(std::string(parameter) + " is not a valid PDF timestamp");
  2820 + }
  2821 + o.attachments_to_add.back().moddate = parameter;
  2822 +}
  2823 +
  2824 +void
  2825 +ArgParser::argAAMimeType(char* parameter)
  2826 +{
  2827 + if (strchr(parameter, '/') == nullptr)
  2828 + {
  2829 + usage("mime type should be specified as type/subtype");
  2830 + }
  2831 + o.attachments_to_add.back().mimetype = parameter;
  2832 +}
  2833 +
  2834 +void
  2835 +ArgParser::argAADescription(char* parameter)
  2836 +{
  2837 + o.attachments_to_add.back().description = parameter;
  2838 +}
  2839 +
  2840 +void
  2841 +ArgParser::argAAReplace()
  2842 +{
  2843 + o.attachments_to_add.back().replace = true;
  2844 +}
  2845 +
  2846 +void
  2847 +ArgParser::argEndAddAttachment()
  2848 +{
  2849 + static std::string now = QUtil::qpdf_time_to_pdf_time(
  2850 + QUtil::get_current_qpdf_time());
  2851 + this->option_table = &(this->main_option_table);
  2852 + auto& cur = o.attachments_to_add.back();
  2853 + if (cur.path.empty())
  2854 + {
  2855 + usage("add attachment: no path specified");
  2856 + }
  2857 + std::string last_element = cur.path;
  2858 + size_t pathsep = cur.path.find_last_of("/\\");
  2859 + if (pathsep != std::string::npos)
  2860 + {
  2861 + last_element = cur.path.substr(pathsep + 1);
  2862 + if (last_element.empty())
  2863 + {
  2864 + usage("path for --add-attachment may not end"
  2865 + " with a path separator");
  2866 + }
  2867 + }
  2868 + if (cur.filename.empty())
  2869 + {
  2870 + cur.filename = last_element;
  2871 + }
  2872 + if (cur.key.empty())
  2873 + {
  2874 + cur.key = last_element;
  2875 + }
  2876 + if (cur.creationdate.empty())
  2877 + {
  2878 + cur.creationdate = now;
  2879 + }
  2880 + if (cur.moddate.empty())
  2881 + {
  2882 + cur.moddate = now;
  2883 + }
  2884 +}
  2885 +
  2886 +void
  2887 +ArgParser::argCApositional(char* arg)
  2888 +{
  2889 + o.attachments_to_copy.back().path = arg;
  2890 +}
  2891 +
  2892 +void
  2893 +ArgParser::argCAprefix(char* parameter)
  2894 +{
  2895 + o.attachments_to_copy.back().prefix = parameter;
  2896 +}
  2897 +
  2898 +void
  2899 +ArgParser::argCApassword(char* parameter)
  2900 +{
  2901 + o.attachments_to_copy.back().password = parameter;
  2902 +}
  2903 +
  2904 +void
  2905 +ArgParser::argEndCopyAttachments()
  2906 +{
  2907 + this->option_table = &(this->main_option_table);
  2908 + if (o.attachments_to_copy.back().path.empty())
  2909 + {
  2910 + usage("copy attachments: no path specified");
  2911 + }
  2912 +}
  2913 +
  2914 +void
2621 2915 ArgParser::handleArgFileArguments()
2622 2916 {
2623 2917 // Support reading arguments from files. Create a new argv. Ensure
... ... @@ -3768,6 +4062,66 @@ static void do_show_pages(QPDF&amp; pdf, Options&amp; o)
3768 4062 }
3769 4063 }
3770 4064  
  4065 +static void do_list_attachments(QPDF& pdf, Options& o)
  4066 +{
  4067 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  4068 + if (efdh.hasEmbeddedFiles())
  4069 + {
  4070 + for (auto const& i: efdh.getEmbeddedFiles())
  4071 + {
  4072 + std::string const& key = i.first;
  4073 + auto efoh = i.second;
  4074 + std::cout << key << " -> "
  4075 + << efoh->getEmbeddedFileStream().getObjGen()
  4076 + << std::endl;
  4077 + if (o.verbose)
  4078 + {
  4079 + auto desc = efoh->getDescription();
  4080 + if (! desc.empty())
  4081 + {
  4082 + std::cout << " description: " << desc << std::endl;
  4083 + }
  4084 + std::cout << " preferred name: " << efoh->getFilename()
  4085 + << std::endl;
  4086 + std::cout << " all names:" << std::endl;
  4087 + for (auto const& i2: efoh->getFilenames())
  4088 + {
  4089 + std::cout << " " << i2.first << " -> " << i2.second
  4090 + << std::endl;
  4091 + }
  4092 + std::cout << " all data streams:" << std::endl;
  4093 + for (auto i2: QPDFDictItems(efoh->getEmbeddedFileStreams()))
  4094 + {
  4095 + std::cout << " " << i2.first << " -> "
  4096 + << i2.second.getObjGen()
  4097 + << std::endl;
  4098 + }
  4099 + }
  4100 + }
  4101 + }
  4102 + else
  4103 + {
  4104 + std::cout << o.infilename << " has no embedded files" << std::endl;
  4105 + }
  4106 +}
  4107 +
  4108 +static void do_show_attachment(QPDF& pdf, Options& o, int& exit_code)
  4109 +{
  4110 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  4111 + auto fs = efdh.getEmbeddedFile(o.attachment_to_show);
  4112 + if (! fs)
  4113 + {
  4114 + std::cerr << whoami << ": attachment " << o.attachment_to_show
  4115 + << " not found" << std::endl;
  4116 + exit_code = EXIT_ERROR;
  4117 + return;
  4118 + }
  4119 + auto efs = fs->getEmbeddedFileStream();
  4120 + QUtil::binary_stdout();
  4121 + Pl_StdioFile out("stdout", stdout);
  4122 + efs.pipeStreamData(&out, 0, qpdf_dl_all);
  4123 +}
  4124 +
3771 4125 static std::set<QPDFObjGen>
3772 4126 get_wanted_json_objects(Options& o)
3773 4127 {
... ... @@ -4354,6 +4708,14 @@ static void do_inspection(QPDF&amp; pdf, Options&amp; o)
4354 4708 {
4355 4709 do_show_pages(pdf, o);
4356 4710 }
  4711 + if (o.list_attachments)
  4712 + {
  4713 + do_list_attachments(pdf, o);
  4714 + }
  4715 + if (! o.attachment_to_show.empty())
  4716 + {
  4717 + do_show_attachment(pdf, o, exit_code);
  4718 + }
4357 4719 if ((! pdf.getWarnings().empty()) && (exit_code != EXIT_ERROR))
4358 4720 {
4359 4721 std::cerr << whoami
... ... @@ -4858,7 +5220,106 @@ static void handle_under_overlay(QPDF&amp; pdf, Options&amp; o)
4858 5220 }
4859 5221 }
4860 5222  
4861   -static void handle_transformations(QPDF& pdf, Options& o)
  5223 +static void maybe_set_pagemode(QPDF& pdf, std::string const& pagemode)
  5224 +{
  5225 + auto root = pdf.getRoot();
  5226 + if (root.getKey("/PageMode").isNull())
  5227 + {
  5228 + root.replaceKey("/PageMode", QPDFObjectHandle::newName(pagemode));
  5229 + }
  5230 +}
  5231 +
  5232 +static void add_attachments(QPDF& pdf, Options& o, int& exit_code)
  5233 +{
  5234 + maybe_set_pagemode(pdf, "/UseAttachments");
  5235 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  5236 + for (auto const& to_add: o.attachments_to_add)
  5237 + {
  5238 + if ((! to_add.replace) && efdh.getEmbeddedFile(to_add.key))
  5239 + {
  5240 + std::cerr << whoami << ": " << pdf.getFilename()
  5241 + << " already has an attachment with key = "
  5242 + << to_add.key << "; use --replace to replace"
  5243 + << " or --key to specificy a different key"
  5244 + << std::endl;
  5245 + exit_code = EXIT_ERROR;
  5246 + continue;
  5247 + }
  5248 +
  5249 + auto fs = QPDFFileSpecObjectHelper::createFileSpec(
  5250 + pdf, to_add.filename, to_add.path);
  5251 + if (! to_add.description.empty())
  5252 + {
  5253 + fs.setDescription(to_add.description);
  5254 + }
  5255 + auto efs = QPDFEFStreamObjectHelper(fs.getEmbeddedFileStream());
  5256 + efs.setCreationDate(to_add.creationdate)
  5257 + .setModDate(to_add.moddate);
  5258 + if (! to_add.mimetype.empty())
  5259 + {
  5260 + efs.setSubtype(to_add.mimetype);
  5261 + }
  5262 +
  5263 + efdh.replaceEmbeddedFile(to_add.key, fs);
  5264 + if (o.verbose)
  5265 + {
  5266 + std::cout << whoami << ": attached " << to_add.path
  5267 + << " as " << to_add.filename
  5268 + << " with key " << to_add.key << std::endl;
  5269 + }
  5270 + }
  5271 +}
  5272 +
  5273 +static void copy_attachments(QPDF& pdf, Options& o, int& exit_code)
  5274 +{
  5275 + maybe_set_pagemode(pdf, "/UseAttachments");
  5276 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  5277 + for (auto const& to_copy: o.attachments_to_copy)
  5278 + {
  5279 + auto other = process_file(
  5280 + to_copy.path.c_str(), to_copy.password.c_str(), o);
  5281 + QPDFEmbeddedFileDocumentHelper other_efdh(*other);
  5282 + auto other_attachments = other_efdh.getEmbeddedFiles();
  5283 + for (auto const& iter: other_attachments)
  5284 + {
  5285 + if (o.verbose)
  5286 + {
  5287 + std::cout << whoami << ": copying attachments from "
  5288 + << to_copy.path << std::endl;
  5289 + }
  5290 + std::string new_key = to_copy.prefix + iter.first;
  5291 + if (efdh.getEmbeddedFile(new_key))
  5292 + {
  5293 + exit_code = EXIT_ERROR;
  5294 + std::cerr << whoami << to_copy.path << " and "
  5295 + << pdf.getFilename()
  5296 + << " both have attachments with key " << new_key
  5297 + << "; use --prefix with --copy-attachments-from"
  5298 + << " or manually copy individual attachments"
  5299 + << std::endl;
  5300 + }
  5301 + else
  5302 + {
  5303 + auto new_fs_oh = pdf.copyForeignObject(
  5304 + iter.second->getObjectHandle());
  5305 + efdh.replaceEmbeddedFile(
  5306 + new_key, QPDFFileSpecObjectHelper(new_fs_oh));
  5307 + if (o.verbose)
  5308 + {
  5309 + std::cout << " " << iter.first << " -> " << new_key
  5310 + << std::endl;
  5311 + }
  5312 + }
  5313 + }
  5314 +
  5315 + if ((other->anyWarnings()) && (exit_code == 0))
  5316 + {
  5317 + exit_code = EXIT_WARNING;
  5318 + }
  5319 + }
  5320 +}
  5321 +
  5322 +static void handle_transformations(QPDF& pdf, Options& o, int& exit_code)
4862 5323 {
4863 5324 QPDFPageDocumentHelper dh(pdf);
4864 5325 if (o.externalize_inline_images)
... ... @@ -4935,6 +5396,35 @@ static void handle_transformations(QPDF&amp; pdf, Options&amp; o)
4935 5396 {
4936 5397 pdf.getRoot().removeKey("/PageLabels");
4937 5398 }
  5399 + if (! o.attachments_to_remove.empty())
  5400 + {
  5401 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  5402 + for (auto const& key: o.attachments_to_remove)
  5403 + {
  5404 + if (efdh.removeEmbeddedFile(key))
  5405 + {
  5406 + if (o.verbose)
  5407 + {
  5408 + std::cout << whoami <<
  5409 + ": removed attachment " << key << std::endl;
  5410 + }
  5411 + }
  5412 + else
  5413 + {
  5414 + std::cerr << whoami <<
  5415 + ": attachment " << key << " not found" << std::endl;
  5416 + exit_code = EXIT_ERROR;
  5417 + }
  5418 + }
  5419 + }
  5420 + if (! o.attachments_to_add.empty())
  5421 + {
  5422 + add_attachments(pdf, o, exit_code);
  5423 + }
  5424 + if (! o.attachments_to_copy.empty())
  5425 + {
  5426 + copy_attachments(pdf, o, exit_code);
  5427 + }
4938 5428 }
4939 5429  
4940 5430 static bool should_remove_unreferenced_resources(QPDF& pdf, Options& o)
... ... @@ -5854,6 +6344,7 @@ int realmain(int argc, char* argv[])
5854 6344 Options o;
5855 6345 ArgParser ap(argc, argv, o);
5856 6346  
  6347 + int exit_code = 0;
5857 6348 try
5858 6349 {
5859 6350 ap.parseOptions();
... ... @@ -5906,7 +6397,7 @@ int realmain(int argc, char* argv[])
5906 6397 handle_rotations(pdf, o);
5907 6398 }
5908 6399 handle_under_overlay(pdf, o);
5909   - handle_transformations(pdf, o);
  6400 + handle_transformations(pdf, o, exit_code);
5910 6401  
5911 6402 if ((o.outfilename == 0) && (! o.replace_input))
5912 6403 {
... ... @@ -5929,7 +6420,10 @@ int realmain(int argc, char* argv[])
5929 6420 << std::endl;
5930 6421 }
5931 6422 // Still return with warning code even if warnings were suppressed.
5932   - return EXIT_WARNING;
  6423 + if (exit_code == 0)
  6424 + {
  6425 + exit_code = EXIT_WARNING;
  6426 + }
5933 6427 }
5934 6428 }
5935 6429 catch (std::exception& e)
... ... @@ -5938,7 +6432,7 @@ int realmain(int argc, char* argv[])
5938 6432 return EXIT_ERROR;
5939 6433 }
5940 6434  
5941   - return 0;
  6435 + return exit_code;
5942 6436 }
5943 6437  
5944 6438 #ifdef WINDOWS_WMAIN
... ...
qpdf/qtest/qpdf.test
... ... @@ -523,7 +523,7 @@ $td-&gt;runtest(&quot;page operations on form xobject&quot;,
523 523 show_ntests();
524 524 # ----------
525 525 $td->notify("--- File Attachments ---");
526   -$n_tests += 4;
  526 +$n_tests += 33;
527 527  
528 528 open(F, ">auto-txt") or die;
529 529 print F "from file";
... ... @@ -532,16 +532,183 @@ $td-&gt;runtest(&quot;attachments&quot;,
532 532 {$td->COMMAND => "test_driver 76 minimal.pdf auto-txt"},
533 533 {$td->FILE => "test76.out", $td->EXIT_STATUS => 0},
534 534 $td->NORMALIZE_NEWLINES);
  535 +$td->runtest("show attachment",
  536 + {$td->COMMAND => "qpdf --show-attachment=att1 a.pdf"},
  537 + {$td->STRING => "from file", $td->EXIT_STATUS => 0},
  538 + $td->NORMALIZE_NEWLINES);
535 539 $td->runtest("check output",
536 540 {$td->FILE => "a.pdf"},
537 541 {$td->FILE => "test76.pdf"});
538   -$td->runtest("attachments",
  542 +$td->runtest("list attachments",
  543 + {$td->COMMAND => "qpdf --list-attachments a.pdf"},
  544 + {$td->FILE => "test76-list.out", $td->EXIT_STATUS => 0},
  545 + $td->NORMALIZE_NEWLINES);
  546 +$td->runtest("list attachments verbose",
  547 + {$td->COMMAND => "qpdf --list-attachments --verbose a.pdf"},
  548 + {$td->FILE => "test76-list-verbose.out", $td->EXIT_STATUS => 0},
  549 + $td->NORMALIZE_NEWLINES);
  550 +$td->runtest("remove attachment (test_driver)",
539 551 {$td->COMMAND => "test_driver 77 test76.pdf"},
540 552 {$td->STRING => "test 77 done\n", $td->EXIT_STATUS => 0},
541 553 $td->NORMALIZE_NEWLINES);
542 554 $td->runtest("check output",
543 555 {$td->FILE => "a.pdf"},
544 556 {$td->FILE => "test77.pdf"});
  557 +$td->runtest("remove attachment (cli)",
  558 + {$td->COMMAND => "qpdf --remove-attachment=att2 test76.pdf" .
  559 + " --static-id --qdf --verbose b.pdf"},
  560 + {$td->FILE => "remove-attachment.out", $td->EXIT_STATUS => 0},
  561 + $td->NORMALIZE_NEWLINES);
  562 +$td->runtest("check output",
  563 + {$td->FILE => "b.pdf"},
  564 + {$td->FILE => "test77.pdf"});
  565 +$td->runtest("show missing attachment",
  566 + {$td->COMMAND => "qpdf --show-attachment=att2 b.pdf"},
  567 + {$td->STRING => "qpdf: attachment att2 not found\n",
  568 + $td->EXIT_STATUS => 2},
  569 + $td->NORMALIZE_NEWLINES);
  570 +$td->runtest("remove missing attachment",
  571 + {$td->COMMAND => "qpdf --remove-attachment=att2 b.pdf c.pdf"},
  572 + {$td->STRING => "qpdf: attachment att2 not found\n",
  573 + $td->EXIT_STATUS => 2},
  574 + $td->NORMALIZE_NEWLINES);
  575 +
  576 +$td->runtest("add attachment: bad creation date",
  577 + {$td->COMMAND => "qpdf minimal.pdf a.pdf" .
  578 + " --add-attachment auto-txt --creationdate=potato --"},
  579 + {$td->REGEXP => ".*potato is not a valid PDF timestamp.*",
  580 + $td->EXIT_STATUS => 2},
  581 + $td->NORMALIZE_NEWLINES);
  582 +$td->runtest("add attachment: bad mod date",
  583 + {$td->COMMAND => "qpdf minimal.pdf a.pdf" .
  584 + " --add-attachment auto-txt --moddate=potato --"},
  585 + {$td->REGEXP => ".*potato is not a valid PDF timestamp.*",
  586 + $td->EXIT_STATUS => 2},
  587 + $td->NORMALIZE_NEWLINES);
  588 +$td->runtest("add attachment: bad mod date",
  589 + {$td->COMMAND => "qpdf minimal.pdf a.pdf" .
  590 + " --add-attachment auto-txt --mimetype=potato --"},
  591 + {$td->REGEXP =>
  592 + ".*mime type should be specified as type/subtype.*",
  593 + $td->EXIT_STATUS => 2},
  594 + $td->NORMALIZE_NEWLINES);
  595 +$td->runtest("add attachment: trailing slash",
  596 + {$td->COMMAND => "qpdf minimal.pdf a.pdf" .
  597 + " --add-attachment auto-txt/ --"},
  598 + {$td->REGEXP => ".*may not end with a path separator.*",
  599 + $td->EXIT_STATUS => 2},
  600 + $td->NORMALIZE_NEWLINES);
  601 +$td->runtest("add attachment: trailing slash",
  602 + {$td->COMMAND => "qpdf minimal.pdf a.pdf" .
  603 + " --add-attachment --"},
  604 + {$td->REGEXP => ".*add attachment: no path specified.*",
  605 + $td->EXIT_STATUS => 2},
  606 + $td->NORMALIZE_NEWLINES);
  607 +
  608 +foreach my $i (qw(1 2 3))
  609 +{
  610 + open(F, ">auto-$i") or die;
  611 + print F "attachment $i";
  612 + close(F);
  613 +}
  614 +my @dates = ("--creationdate=D:20210210091359-05'00'",
  615 + "--moddate=D:20210210141359Z");
  616 +$td->runtest("add attachments",
  617 + {$td->COMMAND =>
  618 + [qw(qpdf minimal.pdf a.pdf --no-original-object-ids),
  619 + qw(--verbose --static-id --qdf),
  620 + qw(--add-attachment ./auto-1), @dates,
  621 + qw(--mimetype=text/plain --),
  622 + qw(--add-attachment ./auto-2 --key=auto-Two), @dates, '--',
  623 + qw(--add-attachment ./auto-3 --filename=auto-Three.txt),
  624 + @dates, '--description=two words', '--']},
  625 + {$td->FILE => "add-attachments-1.out", $td->EXIT_STATUS => 0},
  626 + $td->NORMALIZE_NEWLINES);
  627 +$td->runtest("list attachments",
  628 + {$td->COMMAND => "qpdf --list-attachments a.pdf --verbose"},
  629 + {$td->FILE => "list-attachments-1.out", $td->EXIT_STATUS => 0},
  630 + $td->NORMALIZE_NEWLINES);
  631 +$td->runtest("check output",
  632 + {$td->FILE => "a.pdf"},
  633 + {$td->FILE => "add-attachments-1.pdf"},
  634 + $td->NORMALIZE_NEWLINES);
  635 +$td->runtest("add attachments: duplicate",
  636 + {$td->COMMAND =>
  637 + "qpdf a.pdf b.pdf --verbose --add-attachment ./auto-1 --"},
  638 + {$td->FILE => "add-attachments-duplicate.out",
  639 + $td->EXIT_STATUS => 2},
  640 + $td->NORMALIZE_NEWLINES);
  641 +$td->runtest("add attachments: replace",
  642 + {$td->COMMAND =>
  643 + [qw(qpdf a.pdf b.pdf --no-original-object-ids),
  644 + qw(--verbose --static-id --qdf),
  645 + qw(--add-attachment ./auto-2 --key=auto-1 --replace),
  646 + @dates, '--']},
  647 + {$td->FILE => "add-attachments-2.out", $td->EXIT_STATUS => 0},
  648 + $td->NORMALIZE_NEWLINES);
  649 +$td->runtest("list attachments",
  650 + {$td->COMMAND => "qpdf --list-attachments b.pdf --verbose"},
  651 + {$td->FILE => "list-attachments-3.out", $td->EXIT_STATUS => 0},
  652 + $td->NORMALIZE_NEWLINES);
  653 +$td->runtest("check output",
  654 + {$td->FILE => "b.pdf"},
  655 + {$td->FILE => "add-attachments-2.pdf"},
  656 + $td->NORMALIZE_NEWLINES);
  657 +$td->runtest("copy attachments",
  658 + {$td->COMMAND =>
  659 + "qpdf --verbose --no-original-object-ids" .
  660 + " --static-id --qdf minimal.pdf b.pdf" .
  661 + " --copy-attachments-from a.pdf --"},
  662 + {$td->FILE => "copy-attachments-1.out", $td->EXIT_STATUS => 0},
  663 + $td->NORMALIZE_NEWLINES);
  664 +$td->runtest("list attachments",
  665 + {$td->COMMAND => "qpdf --list-attachments b.pdf --verbose"},
  666 + {$td->FILE => "list-attachments-1.out", $td->EXIT_STATUS => 0},
  667 + $td->NORMALIZE_NEWLINES);
  668 +$td->runtest("check output",
  669 + {$td->FILE => "b.pdf"},
  670 + {$td->FILE => "add-attachments-1.pdf"},
  671 + $td->NORMALIZE_NEWLINES);
  672 +$td->runtest("copy attachments: duplicate",
  673 + {$td->COMMAND =>
  674 + "qpdf --verbose --no-original-object-ids" .
  675 + " --static-id --qdf a.pdf c.pdf" .
  676 + " --copy-attachments-from b.pdf --"},
  677 + {$td->FILE => "copy-attachments-duplicate.out",
  678 + $td->EXIT_STATUS => 2},
  679 + $td->NORMALIZE_NEWLINES);
  680 +$td->runtest("copy attachments: prefix",
  681 + {$td->COMMAND =>
  682 + "qpdf --verbose --no-original-object-ids" .
  683 + " --static-id --qdf a.pdf c.pdf" .
  684 + " --copy-attachments-from b.pdf --prefix=1- --"},
  685 + {$td->FILE => "copy-attachments-2.out", $td->EXIT_STATUS => 0},
  686 + $td->NORMALIZE_NEWLINES);
  687 +$td->runtest("list attachments",
  688 + {$td->COMMAND => "qpdf --list-attachments c.pdf --verbose"},
  689 + {$td->FILE => "list-attachments-2.out", $td->EXIT_STATUS => 0},
  690 + $td->NORMALIZE_NEWLINES);
  691 +$td->runtest("check output",
  692 + {$td->FILE => "c.pdf"},
  693 + {$td->FILE => "copy-attachments-2.pdf"},
  694 + $td->NORMALIZE_NEWLINES);
  695 +$td->runtest("add attachments: current date",
  696 + {$td->COMMAND =>
  697 + [qw(qpdf minimal.pdf a.pdf --encrypt u o 256 --),
  698 + qw(--verbose --add-attachment ./auto-1 --)]},
  699 + {$td->FILE => "add-attachments-3.out", $td->EXIT_STATUS => 0},
  700 + $td->NORMALIZE_NEWLINES);
  701 +$td->runtest("list attachments",
  702 + {$td->COMMAND =>
  703 + "qpdf --password=u --list-attachments a.pdf --verbose"},
  704 + {$td->FILE => "list-attachments-4.out", $td->EXIT_STATUS => 0},
  705 + $td->NORMALIZE_NEWLINES);
  706 +# The object to show here is the one in list-attachments-4.out
  707 +$td->runtest("check dates",
  708 + {$td->COMMAND => "qpdf --show-object=6 a.pdf --password=u"},
  709 + {$td->REGEXP => ".*CreationDate \\(D:\\d+.*ModDate \\(D:\\d+.*",
  710 + $td->EXIT_STATUS => 0},
  711 + $td->NORMALIZE_NEWLINES);
545 712  
546 713 show_ntests();
547 714 # ----------
... ...
qpdf/qtest/qpdf/add-attachments-1.out 0 → 100644
  1 +qpdf: attached ./auto-1 as auto-1 with key auto-1
  2 +qpdf: attached ./auto-2 as auto-2 with key auto-Two
  3 +qpdf: attached ./auto-3 as auto-Three.txt with key auto-3
  4 +qpdf: wrote file a.pdf
... ...
qpdf/qtest/qpdf/add-attachments-1.pdf 0 → 100644
  1 +%PDF-1.3
  2 +%¿÷¢þ
  3 +%QDF-1.0
  4 +
  5 +1 0 obj
  6 +<<
  7 + /Names <<
  8 + /EmbeddedFiles 2 0 R
  9 + >>
  10 + /PageMode /UseAttachments
  11 + /Pages 3 0 R
  12 + /Type /Catalog
  13 +>>
  14 +endobj
  15 +
  16 +2 0 obj
  17 +<<
  18 + /Names [
  19 + (auto-1)
  20 + 4 0 R
  21 + (auto-3)
  22 + 5 0 R
  23 + (auto-Two)
  24 + 6 0 R
  25 + ]
  26 +>>
  27 +endobj
  28 +
  29 +3 0 obj
  30 +<<
  31 + /Count 1
  32 + /Kids [
  33 + 7 0 R
  34 + ]
  35 + /Type /Pages
  36 +>>
  37 +endobj
  38 +
  39 +4 0 obj
  40 +<<
  41 + /EF <<
  42 + /F 8 0 R
  43 + /UF 8 0 R
  44 + >>
  45 + /F (auto-1)
  46 + /Type /Filespec
  47 + /UF (auto-1)
  48 +>>
  49 +endobj
  50 +
  51 +5 0 obj
  52 +<<
  53 + /Desc (two words)
  54 + /EF <<
  55 + /F 10 0 R
  56 + /UF 10 0 R
  57 + >>
  58 + /F (auto-Three.txt)
  59 + /Type /Filespec
  60 + /UF (auto-Three.txt)
  61 +>>
  62 +endobj
  63 +
  64 +6 0 obj
  65 +<<
  66 + /EF <<
  67 + /F 12 0 R
  68 + /UF 12 0 R
  69 + >>
  70 + /F (auto-2)
  71 + /Type /Filespec
  72 + /UF (auto-2)
  73 +>>
  74 +endobj
  75 +
  76 +%% Page 1
  77 +7 0 obj
  78 +<<
  79 + /Contents 14 0 R
  80 + /MediaBox [
  81 + 0
  82 + 0
  83 + 612
  84 + 792
  85 + ]
  86 + /Parent 3 0 R
  87 + /Resources <<
  88 + /Font <<
  89 + /F1 16 0 R
  90 + >>
  91 + /ProcSet 17 0 R
  92 + >>
  93 + /Type /Page
  94 +>>
  95 +endobj
  96 +
  97 +8 0 obj
  98 +<<
  99 + /Params <<
  100 + /CheckSum <a857d18d3fc23ad412122ef040733331>
  101 + /CreationDate (D:20210210091359-05'00')
  102 + /ModDate (D:20210210141359Z)
  103 + /Size 12
  104 + /Subtype /text#2fplain
  105 + >>
  106 + /Type /EmbeddedFile
  107 + /Length 9 0 R
  108 +>>
  109 +stream
  110 +attachment 1
  111 +endstream
  112 +endobj
  113 +
  114 +%QDF: ignore_newline
  115 +9 0 obj
  116 +12
  117 +endobj
  118 +
  119 +10 0 obj
  120 +<<
  121 + /Params <<
  122 + /CheckSum <d6c7ac7cf295ae133fea186cfd068dab>
  123 + /CreationDate (D:20210210091359-05'00')
  124 + /ModDate (D:20210210141359Z)
  125 + /Size 12
  126 + >>
  127 + /Type /EmbeddedFile
  128 + /Length 11 0 R
  129 +>>
  130 +stream
  131 +attachment 3
  132 +endstream
  133 +endobj
  134 +
  135 +%QDF: ignore_newline
  136 +11 0 obj
  137 +12
  138 +endobj
  139 +
  140 +12 0 obj
  141 +<<
  142 + /Params <<
  143 + /CheckSum <9f991a5669c47a94f9350f53e3953e57>
  144 + /CreationDate (D:20210210091359-05'00')
  145 + /ModDate (D:20210210141359Z)
  146 + /Size 12
  147 + >>
  148 + /Type /EmbeddedFile
  149 + /Length 13 0 R
  150 +>>
  151 +stream
  152 +attachment 2
  153 +endstream
  154 +endobj
  155 +
  156 +%QDF: ignore_newline
  157 +13 0 obj
  158 +12
  159 +endobj
  160 +
  161 +%% Contents for page 1
  162 +14 0 obj
  163 +<<
  164 + /Length 15 0 R
  165 +>>
  166 +stream
  167 +BT
  168 + /F1 24 Tf
  169 + 72 720 Td
  170 + (Potato) Tj
  171 +ET
  172 +endstream
  173 +endobj
  174 +
  175 +15 0 obj
  176 +44
  177 +endobj
  178 +
  179 +16 0 obj
  180 +<<
  181 + /BaseFont /Helvetica
  182 + /Encoding /WinAnsiEncoding
  183 + /Name /F1
  184 + /Subtype /Type1
  185 + /Type /Font
  186 +>>
  187 +endobj
  188 +
  189 +17 0 obj
  190 +[
  191 + /PDF
  192 + /Text
  193 +]
  194 +endobj
  195 +
  196 +xref
  197 +0 18
  198 +0000000000 65535 f
  199 +0000000025 00000 n
  200 +0000000149 00000 n
  201 +0000000257 00000 n
  202 +0000000329 00000 n
  203 +0000000439 00000 n
  204 +0000000587 00000 n
  205 +0000000709 00000 n
  206 +0000000904 00000 n
  207 +0000001199 00000 n
  208 +0000001218 00000 n
  209 +0000001488 00000 n
  210 +0000001508 00000 n
  211 +0000001778 00000 n
  212 +0000001821 00000 n
  213 +0000001922 00000 n
  214 +0000001942 00000 n
  215 +0000002061 00000 n
  216 +trailer <<
  217 + /Root 1 0 R
  218 + /Size 18
  219 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>]
  220 +>>
  221 +startxref
  222 +2097
  223 +%%EOF
... ...
qpdf/qtest/qpdf/add-attachments-2.out 0 → 100644
  1 +qpdf: attached ./auto-2 as auto-2 with key auto-1
  2 +qpdf: wrote file b.pdf
... ...
qpdf/qtest/qpdf/add-attachments-2.pdf 0 → 100644
  1 +%PDF-1.3
  2 +%¿÷¢þ
  3 +%QDF-1.0
  4 +
  5 +1 0 obj
  6 +<<
  7 + /Names <<
  8 + /EmbeddedFiles 2 0 R
  9 + >>
  10 + /PageMode /UseAttachments
  11 + /Pages 3 0 R
  12 + /Type /Catalog
  13 +>>
  14 +endobj
  15 +
  16 +2 0 obj
  17 +<<
  18 + /Names [
  19 + (auto-1)
  20 + 4 0 R
  21 + (auto-3)
  22 + 5 0 R
  23 + (auto-Two)
  24 + 6 0 R
  25 + ]
  26 +>>
  27 +endobj
  28 +
  29 +3 0 obj
  30 +<<
  31 + /Count 1
  32 + /Kids [
  33 + 7 0 R
  34 + ]
  35 + /Type /Pages
  36 +>>
  37 +endobj
  38 +
  39 +4 0 obj
  40 +<<
  41 + /EF <<
  42 + /F 8 0 R
  43 + /UF 8 0 R
  44 + >>
  45 + /F (auto-2)
  46 + /Type /Filespec
  47 + /UF (auto-2)
  48 +>>
  49 +endobj
  50 +
  51 +5 0 obj
  52 +<<
  53 + /Desc (two words)
  54 + /EF <<
  55 + /F 10 0 R
  56 + /UF 10 0 R
  57 + >>
  58 + /F (auto-Three.txt)
  59 + /Type /Filespec
  60 + /UF (auto-Three.txt)
  61 +>>
  62 +endobj
  63 +
  64 +6 0 obj
  65 +<<
  66 + /EF <<
  67 + /F 12 0 R
  68 + /UF 12 0 R
  69 + >>
  70 + /F (auto-2)
  71 + /Type /Filespec
  72 + /UF (auto-2)
  73 +>>
  74 +endobj
  75 +
  76 +%% Page 1
  77 +7 0 obj
  78 +<<
  79 + /Contents 14 0 R
  80 + /MediaBox [
  81 + 0
  82 + 0
  83 + 612
  84 + 792
  85 + ]
  86 + /Parent 3 0 R
  87 + /Resources <<
  88 + /Font <<
  89 + /F1 16 0 R
  90 + >>
  91 + /ProcSet 17 0 R
  92 + >>
  93 + /Type /Page
  94 +>>
  95 +endobj
  96 +
  97 +8 0 obj
  98 +<<
  99 + /Params <<
  100 + /CheckSum <9f991a5669c47a94f9350f53e3953e57>
  101 + /CreationDate (D:20210210091359-05'00')
  102 + /ModDate (D:20210210141359Z)
  103 + /Size 12
  104 + >>
  105 + /Type /EmbeddedFile
  106 + /Length 9 0 R
  107 +>>
  108 +stream
  109 +attachment 2
  110 +endstream
  111 +endobj
  112 +
  113 +%QDF: ignore_newline
  114 +9 0 obj
  115 +12
  116 +endobj
  117 +
  118 +10 0 obj
  119 +<<
  120 + /Params <<
  121 + /CheckSum <d6c7ac7cf295ae133fea186cfd068dab>
  122 + /CreationDate (D:20210210091359-05'00')
  123 + /ModDate (D:20210210141359Z)
  124 + /Size 12
  125 + >>
  126 + /Type /EmbeddedFile
  127 + /Length 11 0 R
  128 +>>
  129 +stream
  130 +attachment 3
  131 +endstream
  132 +endobj
  133 +
  134 +%QDF: ignore_newline
  135 +11 0 obj
  136 +12
  137 +endobj
  138 +
  139 +12 0 obj
  140 +<<
  141 + /Params <<
  142 + /CheckSum <9f991a5669c47a94f9350f53e3953e57>
  143 + /CreationDate (D:20210210091359-05'00')
  144 + /ModDate (D:20210210141359Z)
  145 + /Size 12
  146 + >>
  147 + /Type /EmbeddedFile
  148 + /Length 13 0 R
  149 +>>
  150 +stream
  151 +attachment 2
  152 +endstream
  153 +endobj
  154 +
  155 +%QDF: ignore_newline
  156 +13 0 obj
  157 +12
  158 +endobj
  159 +
  160 +%% Contents for page 1
  161 +14 0 obj
  162 +<<
  163 + /Length 15 0 R
  164 +>>
  165 +stream
  166 +BT
  167 + /F1 24 Tf
  168 + 72 720 Td
  169 + (Potato) Tj
  170 +ET
  171 +endstream
  172 +endobj
  173 +
  174 +15 0 obj
  175 +44
  176 +endobj
  177 +
  178 +16 0 obj
  179 +<<
  180 + /BaseFont /Helvetica
  181 + /Encoding /WinAnsiEncoding
  182 + /Name /F1
  183 + /Subtype /Type1
  184 + /Type /Font
  185 +>>
  186 +endobj
  187 +
  188 +17 0 obj
  189 +[
  190 + /PDF
  191 + /Text
  192 +]
  193 +endobj
  194 +
  195 +xref
  196 +0 18
  197 +0000000000 65535 f
  198 +0000000025 00000 n
  199 +0000000149 00000 n
  200 +0000000257 00000 n
  201 +0000000329 00000 n
  202 +0000000439 00000 n
  203 +0000000587 00000 n
  204 +0000000709 00000 n
  205 +0000000904 00000 n
  206 +0000001172 00000 n
  207 +0000001191 00000 n
  208 +0000001461 00000 n
  209 +0000001481 00000 n
  210 +0000001751 00000 n
  211 +0000001794 00000 n
  212 +0000001895 00000 n
  213 +0000001915 00000 n
  214 +0000002034 00000 n
  215 +trailer <<
  216 + /Root 1 0 R
  217 + /Size 18
  218 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>]
  219 +>>
  220 +startxref
  221 +2070
  222 +%%EOF
... ...
qpdf/qtest/qpdf/add-attachments-3.out 0 → 100644
  1 +qpdf: attached ./auto-1 as auto-1 with key auto-1
  2 +qpdf: wrote file a.pdf
... ...
qpdf/qtest/qpdf/add-attachments-duplicate.out 0 → 100644
  1 +qpdf: a.pdf already has an attachment with key = auto-1; use --replace to replace or --key to specificy a different key
  2 +qpdf: wrote file b.pdf
... ...
qpdf/qtest/qpdf/copy-attachments-1.out 0 → 100644
  1 +qpdf: copying attachments from a.pdf
  2 + auto-1 -> auto-1
  3 +qpdf: copying attachments from a.pdf
  4 + auto-3 -> auto-3
  5 +qpdf: copying attachments from a.pdf
  6 + auto-Two -> auto-Two
  7 +qpdf: wrote file b.pdf
... ...
qpdf/qtest/qpdf/copy-attachments-2.out 0 → 100644
  1 +qpdf: copying attachments from b.pdf
  2 + auto-1 -> 1-auto-1
  3 +qpdf: copying attachments from b.pdf
  4 + auto-3 -> 1-auto-3
  5 +qpdf: copying attachments from b.pdf
  6 + auto-Two -> 1-auto-Two
  7 +qpdf: wrote file c.pdf
... ...
qpdf/qtest/qpdf/copy-attachments-2.pdf 0 → 100644
  1 +%PDF-1.3
  2 +%¿÷¢þ
  3 +%QDF-1.0
  4 +
  5 +1 0 obj
  6 +<<
  7 + /Names <<
  8 + /EmbeddedFiles 2 0 R
  9 + >>
  10 + /PageMode /UseAttachments
  11 + /Pages 3 0 R
  12 + /Type /Catalog
  13 +>>
  14 +endobj
  15 +
  16 +2 0 obj
  17 +<<
  18 + /Names [
  19 + (1-auto-1)
  20 + 4 0 R
  21 + (1-auto-3)
  22 + 5 0 R
  23 + (1-auto-Two)
  24 + 6 0 R
  25 + (auto-1)
  26 + 7 0 R
  27 + (auto-3)
  28 + 8 0 R
  29 + (auto-Two)
  30 + 9 0 R
  31 + ]
  32 +>>
  33 +endobj
  34 +
  35 +3 0 obj
  36 +<<
  37 + /Count 1
  38 + /Kids [
  39 + 10 0 R
  40 + ]
  41 + /Type /Pages
  42 +>>
  43 +endobj
  44 +
  45 +4 0 obj
  46 +<<
  47 + /EF <<
  48 + /F 11 0 R
  49 + /UF 11 0 R
  50 + >>
  51 + /F (auto-1)
  52 + /Type /Filespec
  53 + /UF (auto-1)
  54 +>>
  55 +endobj
  56 +
  57 +5 0 obj
  58 +<<
  59 + /Desc (two words)
  60 + /EF <<
  61 + /F 13 0 R
  62 + /UF 13 0 R
  63 + >>
  64 + /F (auto-Three.txt)
  65 + /Type /Filespec
  66 + /UF (auto-Three.txt)
  67 +>>
  68 +endobj
  69 +
  70 +6 0 obj
  71 +<<
  72 + /EF <<
  73 + /F 15 0 R
  74 + /UF 15 0 R
  75 + >>
  76 + /F (auto-2)
  77 + /Type /Filespec
  78 + /UF (auto-2)
  79 +>>
  80 +endobj
  81 +
  82 +7 0 obj
  83 +<<
  84 + /EF <<
  85 + /F 17 0 R
  86 + /UF 17 0 R
  87 + >>
  88 + /F (auto-1)
  89 + /Type /Filespec
  90 + /UF (auto-1)
  91 +>>
  92 +endobj
  93 +
  94 +8 0 obj
  95 +<<
  96 + /Desc (two words)
  97 + /EF <<
  98 + /F 19 0 R
  99 + /UF 19 0 R
  100 + >>
  101 + /F (auto-Three.txt)
  102 + /Type /Filespec
  103 + /UF (auto-Three.txt)
  104 +>>
  105 +endobj
  106 +
  107 +9 0 obj
  108 +<<
  109 + /EF <<
  110 + /F 21 0 R
  111 + /UF 21 0 R
  112 + >>
  113 + /F (auto-2)
  114 + /Type /Filespec
  115 + /UF (auto-2)
  116 +>>
  117 +endobj
  118 +
  119 +%% Page 1
  120 +10 0 obj
  121 +<<
  122 + /Contents 23 0 R
  123 + /MediaBox [
  124 + 0
  125 + 0
  126 + 612
  127 + 792
  128 + ]
  129 + /Parent 3 0 R
  130 + /Resources <<
  131 + /Font <<
  132 + /F1 25 0 R
  133 + >>
  134 + /ProcSet 26 0 R
  135 + >>
  136 + /Type /Page
  137 +>>
  138 +endobj
  139 +
  140 +11 0 obj
  141 +<<
  142 + /Params <<
  143 + /CheckSum <a857d18d3fc23ad412122ef040733331>
  144 + /CreationDate (D:20210210091359-05'00')
  145 + /ModDate (D:20210210141359Z)
  146 + /Size 12
  147 + /Subtype /text#2fplain
  148 + >>
  149 + /Type /EmbeddedFile
  150 + /Length 12 0 R
  151 +>>
  152 +stream
  153 +attachment 1
  154 +endstream
  155 +endobj
  156 +
  157 +%QDF: ignore_newline
  158 +12 0 obj
  159 +12
  160 +endobj
  161 +
  162 +13 0 obj
  163 +<<
  164 + /Params <<
  165 + /CheckSum <d6c7ac7cf295ae133fea186cfd068dab>
  166 + /CreationDate (D:20210210091359-05'00')
  167 + /ModDate (D:20210210141359Z)
  168 + /Size 12
  169 + >>
  170 + /Type /EmbeddedFile
  171 + /Length 14 0 R
  172 +>>
  173 +stream
  174 +attachment 3
  175 +endstream
  176 +endobj
  177 +
  178 +%QDF: ignore_newline
  179 +14 0 obj
  180 +12
  181 +endobj
  182 +
  183 +15 0 obj
  184 +<<
  185 + /Params <<
  186 + /CheckSum <9f991a5669c47a94f9350f53e3953e57>
  187 + /CreationDate (D:20210210091359-05'00')
  188 + /ModDate (D:20210210141359Z)
  189 + /Size 12
  190 + >>
  191 + /Type /EmbeddedFile
  192 + /Length 16 0 R
  193 +>>
  194 +stream
  195 +attachment 2
  196 +endstream
  197 +endobj
  198 +
  199 +%QDF: ignore_newline
  200 +16 0 obj
  201 +12
  202 +endobj
  203 +
  204 +17 0 obj
  205 +<<
  206 + /Params <<
  207 + /CheckSum <a857d18d3fc23ad412122ef040733331>
  208 + /CreationDate (D:20210210091359-05'00')
  209 + /ModDate (D:20210210141359Z)
  210 + /Size 12
  211 + /Subtype /text#2fplain
  212 + >>
  213 + /Type /EmbeddedFile
  214 + /Length 18 0 R
  215 +>>
  216 +stream
  217 +attachment 1
  218 +endstream
  219 +endobj
  220 +
  221 +%QDF: ignore_newline
  222 +18 0 obj
  223 +12
  224 +endobj
  225 +
  226 +19 0 obj
  227 +<<
  228 + /Params <<
  229 + /CheckSum <d6c7ac7cf295ae133fea186cfd068dab>
  230 + /CreationDate (D:20210210091359-05'00')
  231 + /ModDate (D:20210210141359Z)
  232 + /Size 12
  233 + >>
  234 + /Type /EmbeddedFile
  235 + /Length 20 0 R
  236 +>>
  237 +stream
  238 +attachment 3
  239 +endstream
  240 +endobj
  241 +
  242 +%QDF: ignore_newline
  243 +20 0 obj
  244 +12
  245 +endobj
  246 +
  247 +21 0 obj
  248 +<<
  249 + /Params <<
  250 + /CheckSum <9f991a5669c47a94f9350f53e3953e57>
  251 + /CreationDate (D:20210210091359-05'00')
  252 + /ModDate (D:20210210141359Z)
  253 + /Size 12
  254 + >>
  255 + /Type /EmbeddedFile
  256 + /Length 22 0 R
  257 +>>
  258 +stream
  259 +attachment 2
  260 +endstream
  261 +endobj
  262 +
  263 +%QDF: ignore_newline
  264 +22 0 obj
  265 +12
  266 +endobj
  267 +
  268 +%% Contents for page 1
  269 +23 0 obj
  270 +<<
  271 + /Length 24 0 R
  272 +>>
  273 +stream
  274 +BT
  275 + /F1 24 Tf
  276 + 72 720 Td
  277 + (Potato) Tj
  278 +ET
  279 +endstream
  280 +endobj
  281 +
  282 +24 0 obj
  283 +44
  284 +endobj
  285 +
  286 +25 0 obj
  287 +<<
  288 + /BaseFont /Helvetica
  289 + /Encoding /WinAnsiEncoding
  290 + /Name /F1
  291 + /Subtype /Type1
  292 + /Type /Font
  293 +>>
  294 +endobj
  295 +
  296 +26 0 obj
  297 +[
  298 + /PDF
  299 + /Text
  300 +]
  301 +endobj
  302 +
  303 +xref
  304 +0 27
  305 +0000000000 65535 f
  306 +0000000025 00000 n
  307 +0000000149 00000 n
  308 +0000000334 00000 n
  309 +0000000407 00000 n
  310 +0000000519 00000 n
  311 +0000000667 00000 n
  312 +0000000779 00000 n
  313 +0000000891 00000 n
  314 +0000001039 00000 n
  315 +0000001161 00000 n
  316 +0000001357 00000 n
  317 +0000001654 00000 n
  318 +0000001674 00000 n
  319 +0000001944 00000 n
  320 +0000001964 00000 n
  321 +0000002234 00000 n
  322 +0000002254 00000 n
  323 +0000002551 00000 n
  324 +0000002571 00000 n
  325 +0000002841 00000 n
  326 +0000002861 00000 n
  327 +0000003131 00000 n
  328 +0000003174 00000 n
  329 +0000003275 00000 n
  330 +0000003295 00000 n
  331 +0000003414 00000 n
  332 +trailer <<
  333 + /Root 1 0 R
  334 + /Size 27
  335 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>]
  336 +>>
  337 +startxref
  338 +3450
  339 +%%EOF
... ...
qpdf/qtest/qpdf/copy-attachments-duplicate.out 0 → 100644
  1 +qpdf: copying attachments from b.pdf
  2 +qpdfb.pdf and a.pdf both have attachments with key auto-1; use --prefix with --copy-attachments-from or manually copy individual attachments
  3 +qpdf: copying attachments from b.pdf
  4 +qpdfb.pdf and a.pdf both have attachments with key auto-3; use --prefix with --copy-attachments-from or manually copy individual attachments
  5 +qpdf: copying attachments from b.pdf
  6 +qpdfb.pdf and a.pdf both have attachments with key auto-Two; use --prefix with --copy-attachments-from or manually copy individual attachments
  7 +qpdf: wrote file c.pdf
... ...
qpdf/qtest/qpdf/list-attachments-1.out 0 → 100644
  1 +auto-1 -> 8,0
  2 + preferred name: auto-1
  3 + all names:
  4 + /F -> auto-1
  5 + /UF -> auto-1
  6 + all data streams:
  7 + /F -> 8,0
  8 + /UF -> 8,0
  9 +auto-3 -> 10,0
  10 + description: two words
  11 + preferred name: auto-Three.txt
  12 + all names:
  13 + /F -> auto-Three.txt
  14 + /UF -> auto-Three.txt
  15 + all data streams:
  16 + /F -> 10,0
  17 + /UF -> 10,0
  18 +auto-Two -> 12,0
  19 + preferred name: auto-2
  20 + all names:
  21 + /F -> auto-2
  22 + /UF -> auto-2
  23 + all data streams:
  24 + /F -> 12,0
  25 + /UF -> 12,0
... ...
qpdf/qtest/qpdf/list-attachments-2.out 0 → 100644
  1 +1-auto-1 -> 11,0
  2 + preferred name: auto-1
  3 + all names:
  4 + /F -> auto-1
  5 + /UF -> auto-1
  6 + all data streams:
  7 + /F -> 11,0
  8 + /UF -> 11,0
  9 +1-auto-3 -> 13,0
  10 + description: two words
  11 + preferred name: auto-Three.txt
  12 + all names:
  13 + /F -> auto-Three.txt
  14 + /UF -> auto-Three.txt
  15 + all data streams:
  16 + /F -> 13,0
  17 + /UF -> 13,0
  18 +1-auto-Two -> 15,0
  19 + preferred name: auto-2
  20 + all names:
  21 + /F -> auto-2
  22 + /UF -> auto-2
  23 + all data streams:
  24 + /F -> 15,0
  25 + /UF -> 15,0
  26 +auto-1 -> 17,0
  27 + preferred name: auto-1
  28 + all names:
  29 + /F -> auto-1
  30 + /UF -> auto-1
  31 + all data streams:
  32 + /F -> 17,0
  33 + /UF -> 17,0
  34 +auto-3 -> 19,0
  35 + description: two words
  36 + preferred name: auto-Three.txt
  37 + all names:
  38 + /F -> auto-Three.txt
  39 + /UF -> auto-Three.txt
  40 + all data streams:
  41 + /F -> 19,0
  42 + /UF -> 19,0
  43 +auto-Two -> 21,0
  44 + preferred name: auto-2
  45 + all names:
  46 + /F -> auto-2
  47 + /UF -> auto-2
  48 + all data streams:
  49 + /F -> 21,0
  50 + /UF -> 21,0
... ...
qpdf/qtest/qpdf/list-attachments-3.out 0 → 100644
  1 +auto-1 -> 8,0
  2 + preferred name: auto-2
  3 + all names:
  4 + /F -> auto-2
  5 + /UF -> auto-2
  6 + all data streams:
  7 + /F -> 8,0
  8 + /UF -> 8,0
  9 +auto-3 -> 10,0
  10 + description: two words
  11 + preferred name: auto-Three.txt
  12 + all names:
  13 + /F -> auto-Three.txt
  14 + /UF -> auto-Three.txt
  15 + all data streams:
  16 + /F -> 10,0
  17 + /UF -> 10,0
  18 +auto-Two -> 12,0
  19 + preferred name: auto-2
  20 + all names:
  21 + /F -> auto-2
  22 + /UF -> auto-2
  23 + all data streams:
  24 + /F -> 12,0
  25 + /UF -> 12,0
... ...
qpdf/qtest/qpdf/list-attachments-4.out 0 → 100644
  1 +auto-1 -> 6,0
  2 + preferred name: auto-1
  3 + all names:
  4 + /F -> auto-1
  5 + /UF -> auto-1
  6 + all data streams:
  7 + /F -> 6,0
  8 + /UF -> 6,0
... ...
qpdf/qtest/qpdf/remove-attachment.out 0 → 100644
  1 +qpdf: removed attachment att2
  2 +qpdf: wrote file b.pdf
... ...
qpdf/qtest/qpdf/test76-list-verbose.out 0 → 100644
  1 +att1 -> 8,0
  2 + description: some text
  3 + preferred name: att1.txt
  4 + all names:
  5 + /F -> att1.txt
  6 + /UF -> att1.txt
  7 + all data streams:
  8 + /F -> 8,0
  9 + /UF -> 8,0
  10 +att2 -> 10,0
  11 + preferred name: att2.txt
  12 + all names:
  13 + /F -> att2.txt
  14 + /UF -> att2.txt
  15 + all data streams:
  16 + /F -> 10,0
  17 + /UF -> 10,0
  18 +att3 -> 12,0
  19 + preferred name: π.txt
  20 + all names:
  21 + /F -> att3.txt
  22 + /UF -> π.txt
  23 + all data streams:
  24 + /F -> 12,0
  25 + /UF -> 12,0
... ...
qpdf/qtest/qpdf/test76-list.out 0 → 100644
  1 +att1 -> 8,0
  2 +att2 -> 10,0
  3 +att3 -> 12,0
... ...