diff --git a/ChangeLog b/ChangeLog index 676a2466..2a3f1780 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2021-02-18 Jay Berkenbilt + + * Add examples/pdf-attach-file.cc to illustrate new file + attachment method and also new parse that takes indirect objects. + 2021-02-17 Jay Berkenbilt * Allow optional numeric argument to --collate. If --collate=n is diff --git a/TODO b/TODO index 06ee7a61..1e8ab5d9 100644 --- a/TODO +++ b/TODO @@ -249,6 +249,10 @@ directory or that are otherwise not publicly accessible. This includes things sent to me by email that are specifically not public. Even so, I find it useful to make reference to them in this list. + * Add code for creation of a file attachment annotation. It should + also be possible to create a widget annotation and a form field. + Update the pdf-attach-file.cc example with new APIs when ready. + * If I do more with json, take a look at this C++ header-only JSON library: https://github.com/nlohmann/json/releases diff --git a/examples/build.mk b/examples/build.mk index bf730023..0d2c1132 100644 --- a/examples/build.mk +++ b/examples/build.mk @@ -1,4 +1,5 @@ BINS_examples = \ + pdf-attach-file \ pdf-bookmarks \ pdf-count-strings \ pdf-create \ diff --git a/examples/pdf-attach-file.cc b/examples/pdf-attach-file.cc new file mode 100644 index 00000000..903f30eb --- /dev/null +++ b/examples/pdf-attach-file.cc @@ -0,0 +1,234 @@ +#include +#include +#include +#include +#include + +#include +#include + +// +// This example attaches a file to an input file, adds a page to the +// beginning of the file that includes a file attachment annotation, +// and writes the result to an output file. It also illustrates a +// number of new API calls that were added in qpdf 10.2. +// + +static char const* whoami = 0; + +static void usage(std::string const& msg) +{ + std::cerr << msg << std::endl << std::endl + << "Usage: " << whoami << " options" << std::endl + << "Options:" << std::endl + << " --infile infile.pdf" << std::endl + << " --outfile outfile.pdf" << std::endl + << " --attachment attachment" << std::endl + << " [ --password infile-password ]" << std::endl + << " [ --mimetype attachment mime type ]" << std::endl; + exit(2); +} + +static void process(char const* infilename, char const* password, + char const* attachment, char const* mimetype, + char const* outfilename) +{ + QPDF q; + q.processFile(infilename, password); + + // Create an indirect object for the built-in Helvetica font. + auto f1 = q.makeIndirectObject( + QPDFObjectHandle::parse( + "<<" + " /Type /Font" + " /Subtype /Type1" + " /Name /F1" + " /BaseFont /Helvetica" + " /Encoding /WinAnsiEncoding" + ">>")); + + // Create a resources dictionary with fonts. This uses the new + // parse introduced in qpdf 10.2 that takes a QPDF* and allows + // indirect object references. + auto resources = q.makeIndirectObject( + QPDFObjectHandle::parse( + &q, + "<<" + " /Font <<" + " /F1 " + f1.unparse() + + " >>" + ">>")); + + // Create a file spec. + std::string key(attachment); + size_t pos = key.find_last_of("/\\"); + if (pos != std::string::npos) + { + key = key.substr(pos + 1); + } + if (key.empty()) + { + throw std::runtime_error("can't get last path element of attachment"); + } + std::cout << whoami << ": attaching " << attachment << " as " << key + << std::endl; + auto fs = QPDFFileSpecObjectHelper::createFileSpec(q, key, attachment); + + if (mimetype) + { + // Get an embedded file stream and set mimetype + auto ef = QPDFEFStreamObjectHelper(fs.getEmbeddedFileStream()); + ef.setSubtype(mimetype); + } + + // Add the embedded file at the document level as an attachment. + auto efdh = QPDFEmbeddedFileDocumentHelper(q); + efdh.replaceEmbeddedFile(key, fs); + + // Create a file attachment annotation. + + // Create appearance stream for the attachment. + + auto ap = QPDFObjectHandle::newStream( + &q, + "0 10 m\n" + "10 0 l\n" + "20 10 l\n" + "10 0 m\n" + "10 20 l\n" + "0 0 20 20 re\n" + "S\n"); + auto apdict = ap.getDict(); + apdict.replaceKey("/Resources", QPDFObjectHandle::newDictionary()); + apdict.replaceKey("/Type", QPDFObjectHandle::newName("/XObject")); + apdict.replaceKey("/Subtype", QPDFObjectHandle::newName("/Form")); + apdict.replaceKey("/BBox", QPDFObjectHandle::parse("[ 0 0 20 20 ]")); + auto annot = q.makeIndirectObject( + QPDFObjectHandle::parse( + &q, + "<<" + " /AP <<" + " /N " + ap.unparse() + + " >>" + " /Contents " + + QPDFObjectHandle::newUnicodeString(attachment).unparse() + + " /FS " + fs.getObjectHandle().unparse() + + " /NM " + + QPDFObjectHandle::newUnicodeString(attachment).unparse() + + " /Rect [ 72 700 92 720 ]" + " /Subtype /FileAttachment" + " /Type /Annot" + ">>")); + + // Generate contents for the page. + auto contents = QPDFObjectHandle::newStream( + &q, + "q\n" + "BT\n" + " 102 700 Td\n" + " /F1 16 Tf\n" + " (Here is an attachment.) Tj\n" + "ET\n" + "Q\n"); + + // Create the page object. + auto page = QPDFObjectHandle::parse( + &q, + "<<" + " /Annots [ " + annot.unparse() + " ]" + " /Contents " + contents.unparse() + + " /MediaBox [0 0 612 792]" + " /Resources " + resources.unparse() + + " /Type /Page" + ">>"); + + // Add the page. + q.addPage(page, true); + + QPDFWriter w(q, outfilename); + w.setQDFMode(true); + w.setSuppressOriginalObjectIDs(true); + w.setDeterministicID(true); + w.write(); +} + +int main(int argc, char* argv[]) +{ + whoami = QUtil::getWhoami(argv[0]); + + // For libtool's sake.... + if (strncmp(whoami, "lt-", 3) == 0) + { + whoami += 3; + } + + char const* infilename = 0; + char const* password = 0; + char const* attachment = 0; + char const* outfilename = 0; + char const* mimetype = 0; + + auto check_arg = [](char const* arg, std::string const& msg) { + if (arg == nullptr) + { + usage(msg); + } + }; + + for (int i = 1; i < argc; ++i) + { + char* arg = argv[i]; + char* next = argv[i+1]; + if (strcmp(arg, "--infile") == 0) + { + check_arg(next, "--infile takes an argument"); + infilename = next; + ++i; + } + else if (strcmp(arg, "--password") == 0) + { + check_arg(next, "--password takes an argument"); + password = next; + ++i; + } + else if (strcmp(arg, "--attachment") == 0) + { + check_arg(next, "--attachment takes an argument"); + attachment = next; + ++i; + } + else if (strcmp(arg, "--outfile") == 0) + { + check_arg(next, "--outfile takes an argument"); + outfilename = next; + ++i; + } + else if (strcmp(arg, "--mimetype") == 0) + { + check_arg(next, "--mimetype takes an argument"); + mimetype = next; + ++i; + } + else + { + usage("unknown argument " + std::string(arg)); + } + } + if (! (infilename && attachment && outfilename)) + { + usage("required arguments were not provided"); + } + + try + { + process(infilename, password, attachment, mimetype, outfilename); + } + catch (std::exception &e) + { + std::cerr << whoami << " exception: " + << e.what() << std::endl; + exit(2); + } + + return 0; +} diff --git a/examples/qtest/attach-file.test b/examples/qtest/attach-file.test new file mode 100644 index 00000000..9d8e7504 --- /dev/null +++ b/examples/qtest/attach-file.test @@ -0,0 +1,35 @@ +#!/usr/bin/env perl +require 5.008; +use warnings; +use strict; + +chdir("attach-file") or die "chdir testdir failed: $!\n"; + +require TestDriver; + +cleanup(); + +my $td = new TestDriver('attach-file'); + +$td->runtest("attach file", + {$td->COMMAND => + "pdf-attach-file --infile input.pdf" . + " --attachment ./potato.png" . + " --outfile a.pdf" . + " --mimetype image/png"}, + {$td->STRING => + "pdf-attach-file: attaching ./potato.png as potato.png\n", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "output.pdf"}); + +cleanup(); + +$td->report(2); + +sub cleanup +{ + unlink "a.pdf"; +} diff --git a/examples/qtest/attach-file/input.pdf b/examples/qtest/attach-file/input.pdf new file mode 100644 index 00000000..a7e01f91 --- /dev/null +++ b/examples/qtest/attach-file/input.pdf @@ -0,0 +1,79 @@ +%PDF-1.3 +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [ + 3 0 R + ] + /Count 1 +>> +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /ProcSet 5 0 R + /Font << + /F1 6 0 R + >> + >> +>> +endobj + +4 0 obj +<< + /Length 44 +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +5 0 obj +[ + /PDF + /Text +] +endobj + +6 0 obj +<< + /Type /Font + /Subtype /Type1 + /Name /F1 + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding +>> +endobj + +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000063 00000 n +0000000135 00000 n +0000000307 00000 n +0000000403 00000 n +0000000438 00000 n +trailer << + /Size 7 + /Root 1 0 R +>> +startxref +556 +%%EOF diff --git a/examples/qtest/attach-file/output.pdf b/examples/qtest/attach-file/output.pdf new file mode 100644 index 00000000..d41cef4f Binary files /dev/null and b/examples/qtest/attach-file/output.pdf differ diff --git a/examples/qtest/attach-file/potato.png b/examples/qtest/attach-file/potato.png new file mode 100644 index 00000000..da425324 Binary files /dev/null and b/examples/qtest/attach-file/potato.png differ diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 144c4edf..4a294cb3 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -5209,7 +5209,9 @@ print "\n"; QPDFEmbeddedFileDocumentHelper, QPDFFileSpecObjectHelper, and QPDFEFStreamObjectHelper. See their - respective headers for details. + respective headers for details and + examples/pdf-attach-file.cc for an + example. @@ -5231,6 +5233,8 @@ print "\n"; QPDFObjectHandle::parse that takes a QPDF pointer as context so that it can parse strings containing indirect object references. + This is illustrated in + examples/pdf-attach-file.cc.