From c2023db265ea35ad7d0ab0cd989f16479bcb798d Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Mon, 5 Oct 2009 00:42:48 +0000 Subject: [PATCH] Implement changes suggested by Zarko and our subsequent conversations: - Add a way to set the minimum PDF version - Add a way to force the PDF version - Have isEncrypted return true if an /Encrypt dictionary exists even when we can't read the file - Allow qpdf_init_write to be called multiple times - Update some comments in headers git-svn-id: svn+q:///qpdf/trunk@748 71b93d88-0707-0410-a8cf-f5a4172ac649 --- ChangeLog | 4 ++ TODO | 12 ----- include/qpdf/QPDFWriter.hh | 33 +++++++++++++- include/qpdf/qpdf-c.h | 13 +++++- libqpdf/QPDFWriter.cc | 50 ++++++++++++++++----- libqpdf/QPDF_encryption.cc | 6 ++- libqpdf/qpdf-c.cc | 20 +++++++++ qpdf/qpdf-ctest.c | 71 ++++++++++++++++++++++-------- qpdf/qpdf.cc | 36 +++++++++++++++ qpdf/qpdf.testcov | 5 +++ qpdf/qtest/qpdf.test | 42 +++++++++++++++++- qpdf/qtest/qpdf/forced-version.out | 5 +++ qpdf/qtest/qpdf/min-version.out | 5 +++ 13 files changed, 255 insertions(+), 47 deletions(-) create mode 100644 qpdf/qtest/qpdf/forced-version.out create mode 100644 qpdf/qtest/qpdf/min-version.out diff --git a/ChangeLog b/ChangeLog index e21ad6b8..0f18cfc6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ 2009-10-04 Jay Berkenbilt + * Add methods to QPDFWriter and corresponding command line + arguments to qpdf to set the minimum output PDF version and also + to force the version to a particular value. + * libqpdf/QPDF.cc (processXRefStream): warn and ignore extra xref stream entries when stream is larger than reported size. This used to be a fatal error. (Fixes qpdf-Bugs-2872265.) diff --git a/TODO b/TODO index 796fbb11..19ad50e3 100644 --- a/TODO +++ b/TODO @@ -1,15 +1,3 @@ -Now -=== - - * Add functions to set minimum version and to force pdf version - - * Make multiple calls to init_write safe; document that write - parameter settings must be repeated - - * qpdf_is_encrypted returns false for encrypted file when incorrect - password is given - - 2.1 === diff --git a/include/qpdf/QPDFWriter.hh b/include/qpdf/QPDFWriter.hh index 7d62e476..b5f23457 100644 --- a/include/qpdf/QPDFWriter.hh +++ b/include/qpdf/QPDFWriter.hh @@ -34,7 +34,13 @@ class Pl_Count; class QPDFWriter { public: - // Passing null as filename means write to stdout + // Passing null as filename means write to stdout. QPDFWriter + // will create a zero-length output file upon construction. If + // write fails, the empty or partially written file will not be + // deleted. This is by design: sometimes the partial file may be + // useful for tracking down problems. If your application doesn't + // want the partially written file to be left behind, you should + // delete it the eventual call to write fails. DLL_EXPORT QPDFWriter(QPDF& pdf, char const* filename); DLL_EXPORT @@ -78,6 +84,30 @@ class QPDFWriter DLL_EXPORT void setQDFMode(bool); + // Set the minimum PDF version. If the PDF version of the input + // file (or previously set minimum version) is less than the + // version passed to this method, the PDF version of the output + // file will be set to this value. If the original PDF file's + // version or previously set minimum version is already this + // version or later, the original file's version will be used. + // QPDFWriter automatically sets the minimum version to 1.4 when + // R3 encryption parameters are used, and to 1.5 when object + // streams are used. + DLL_EXPORT + void setMinimumPDFVersion(std::string const&); + + // Force the PDF version of the output file to be a given version. + // Use of this function may create PDF files that will not work + // properly with older PDF viewers. When a PDF version is set + // using this function, qpdf will use this version even if the + // file contains features that are not supported in that version + // of PDF. In other words, you should only use this function if + // you are sure the PDF file in question has no features of newer + // versions of PDF or if you are willing to create files that old + // viewers may try to open but not be able to properly interpret. + DLL_EXPORT + void forcePDFVersion(std::string const&); + // Cause a static /ID value to be generated. Use only in test // suites. DLL_EXPORT @@ -241,6 +271,7 @@ class QPDFWriter std::string id1; // for /ID key of std::string id2; // trailer dictionary std::string min_pdf_version; + std::string forced_pdf_version; int encryption_dict_objid; std::string cur_data_key; std::list > to_delete; diff --git a/include/qpdf/qpdf-c.h b/include/qpdf/qpdf-c.h index 11dae4f1..fab8112b 100644 --- a/include/qpdf/qpdf-c.h +++ b/include/qpdf/qpdf-c.h @@ -189,7 +189,12 @@ extern "C" { /* Supply the name of the file to be written and initialize the * qpdf_data object to handle writing operations. This function * also attempts to create the file. The PDF data is not written - * until the call to qpdf_write. + * until the call to qpdf_write. qpdf_init_write may be called + * multiple times for the same qpdf_data object. When + * qpdf_init_write is called, all information from previous calls + * to functions that set write parameters (qpdf_set_linearization, + * etc.) is lost, so any write parameter functions must be called + * again. */ DLL_EXPORT QPDF_ERROR_CODE qpdf_init_write(qpdf_data qpdf, char const* filename); @@ -256,6 +261,12 @@ extern "C" { DLL_EXPORT void qpdf_set_linearization(qpdf_data qpdf, QPDF_BOOL value); + DLL_EXPORT + void qpdf_set_minimum_pdf_version(qpdf_data qpdf, char const* version); + + DLL_EXPORT + void qpdf_force_pdf_version(qpdf_data qpdf, char const* version); + /* Do actual write operation. */ DLL_EXPORT QPDF_ERROR_CODE qpdf_write(qpdf_data qpdf); diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index 4191e906..0ede7889 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -99,6 +99,37 @@ QPDFWriter::setQDFMode(bool val) this->qdf_mode = val; } +void +QPDFWriter::setMinimumPDFVersion(std::string const& version) +{ + bool set_version = false; + if (this->min_pdf_version.empty()) + { + set_version = true; + } + else + { + float v = atof(version.c_str()); + float mv = atof(this->min_pdf_version.c_str()); + if (v > mv) + { + QTC::TC("qpdf", "QPDFWriter increasing minimum version"); + set_version = true; + } + } + + if (set_version) + { + this->min_pdf_version = version; + } +} + +void +QPDFWriter::forcePDFVersion(std::string const& version) +{ + this->forced_pdf_version = version; +} + void QPDFWriter::setStaticID(bool val) { @@ -147,7 +178,7 @@ QPDFWriter::setR2EncryptionParameters( clear.insert(6); } - this->min_pdf_version = "1.3"; + setMinimumPDFVersion("1.3"); setEncryptionParameters(user_password, owner_password, 1, 2, 5, clear); } @@ -221,7 +252,7 @@ QPDFWriter::setR3EncryptionParameters( // no default so gcc warns for missing cases } - this->min_pdf_version = "1.4"; + setMinimumPDFVersion("1.4"); setEncryptionParameters(user_password, owner_password, 2, 3, 16, clear); } @@ -1361,7 +1392,7 @@ QPDFWriter::write() if (! this->object_stream_to_objects.empty()) { - this->min_pdf_version = "1.5"; + setMinimumPDFVersion("1.5"); } generateID(); @@ -1417,15 +1448,12 @@ QPDFWriter::writeEncryptionDictionary() void QPDFWriter::writeHeader() { - std::string version = pdf.getPDFVersion(); - if (! this->min_pdf_version.empty()) + setMinimumPDFVersion(pdf.getPDFVersion()); + std::string version = this->min_pdf_version; + if (! this->forced_pdf_version.empty()) { - float ov = atof(version.c_str()); - float mv = atof(this->min_pdf_version.c_str()); - if (mv > ov) - { - version = this->min_pdf_version; - } + QTC::TC("qpdf", "QPDFWriter using forced PDF version"); + version = this->forced_pdf_version; } writeString("%PDF-"); diff --git a/libqpdf/QPDF_encryption.cc b/libqpdf/QPDF_encryption.cc index 5d061dfd..075ab22a 100644 --- a/libqpdf/QPDF_encryption.cc +++ b/libqpdf/QPDF_encryption.cc @@ -289,6 +289,11 @@ QPDF::initializeEncryption() return; } + // Go ahead and set this->encryption here. That way, isEncrypted + // will return true even if there were errors reading the + // encryption dictionary. + this->encrypted = true; + QPDFObjectHandle id_obj = this->trailer.getKey("/ID"); if (! (id_obj.isArray() && (id_obj.getArrayNItems() == 2) && @@ -377,7 +382,6 @@ QPDF::initializeEncryption() throw QPDFExc(this->file.getName() + ": invalid password"); } - this->encrypted = true; this->encryption_key = compute_encryption_key(this->user_password, data); } diff --git a/libqpdf/qpdf-c.cc b/libqpdf/qpdf-c.cc index 9e2d0cbf..966d3dbb 100644 --- a/libqpdf/qpdf-c.cc +++ b/libqpdf/qpdf-c.cc @@ -252,6 +252,12 @@ DLL_EXPORT QPDF_ERROR_CODE qpdf_init_write(qpdf_data qpdf, char const* filename) { QPDF_ERROR_CODE status = QPDF_SUCCESS; + if (qpdf->qpdf_writer) + { + QTC::TC("qpdf", "qpdf-c called qpdf_init_write multiple times"); + delete qpdf->qpdf_writer; + qpdf->qpdf_writer = 0; + } try { qpdf->qpdf_writer = new QPDFWriter(*(qpdf->qpdf), filename); @@ -390,6 +396,20 @@ void qpdf_set_linearization(qpdf_data qpdf, QPDF_BOOL value) qpdf->qpdf_writer->setLinearization(value); } +DLL_EXPORT +void qpdf_set_minimum_pdf_version(qpdf_data qpdf, char const* version) +{ + QTC::TC("qpdf", "qpdf-c called qpdf_set_minimum_pdf_version"); + qpdf->qpdf_writer->setMinimumPDFVersion(version); +} + +DLL_EXPORT +void qpdf_force_pdf_version(qpdf_data qpdf, char const* version) +{ + QTC::TC("qpdf", "qpdf-c called qpdf_force_pdf_version"); + qpdf->qpdf_writer->forcePDFVersion(version); +} + DLL_EXPORT QPDF_ERROR_CODE qpdf_write(qpdf_data qpdf) { diff --git a/qpdf/qpdf-ctest.c b/qpdf/qpdf-ctest.c index 42d8f9f4..c3e1040b 100644 --- a/qpdf/qpdf-ctest.c +++ b/qpdf/qpdf-ctest.c @@ -20,7 +20,8 @@ static void report_errors() static void test01(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); printf("version: %s\n", qpdf_get_pdf_version(qpdf)); @@ -53,7 +54,8 @@ static void test01(char const* infile, static void test02(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_set_suppress_warnings(qpdf, QPDF_TRUE); if (((qpdf_read(qpdf, infile, password) & QPDF_ERRORS) == 0) && @@ -67,7 +69,8 @@ static void test02(char const* infile, static void test03(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -79,7 +82,8 @@ static void test03(char const* infile, static void test04(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_set_ignore_xref_streams(qpdf, QPDF_TRUE); qpdf_read(qpdf, infile, password); @@ -91,7 +95,8 @@ static void test04(char const* infile, static void test05(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -103,7 +108,8 @@ static void test05(char const* infile, static void test06(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -115,7 +121,8 @@ static void test06(char const* infile, static void test07(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -127,7 +134,8 @@ static void test07(char const* infile, static void test08(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -140,7 +148,8 @@ static void test08(char const* infile, static void test09(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -152,7 +161,8 @@ static void test09(char const* infile, static void test10(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_set_attempt_recovery(qpdf, QPDF_FALSE); qpdf_read(qpdf, infile, password); @@ -161,7 +171,8 @@ static void test10(char const* infile, static void test11(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -174,7 +185,8 @@ static void test11(char const* infile, static void test12(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); qpdf_init_write(qpdf, outfile); @@ -188,7 +200,8 @@ static void test12(char const* infile, static void test13(char const* infile, char const* password, - char const* outfile) + char const* outfile, + char const* outfile2) { qpdf_read(qpdf, infile, password); printf("user password: %s\n", qpdf_get_user_password(qpdf)); @@ -199,15 +212,33 @@ static void test13(char const* infile, report_errors(); } +static void test14(char const* infile, + char const* password, + char const* outfile, + char const* outfile2) +{ + qpdf_read(qpdf, infile, password); + qpdf_init_write(qpdf, outfile); + qpdf_set_static_ID(qpdf, QPDF_TRUE); + qpdf_set_minimum_pdf_version(qpdf, "1.6"); + qpdf_write(qpdf); + qpdf_init_write(qpdf, outfile2); + qpdf_set_static_ID(qpdf, QPDF_TRUE); + qpdf_force_pdf_version(qpdf, "1.4"); + qpdf_write(qpdf); + report_errors(); +} + int main(int argc, char* argv[]) { char* whoami = 0; char* p = 0; int n = 0; - char const* infile; - char const* password; - char const* outfile; - void (*fn)(char const*, char const*, char const*) = 0; + char const* infile = 0; + char const* password = 0; + char const* outfile = 0; + char const* outfile2 = 0; + void (*fn)(char const*, char const*, char const*, char const*) = 0; if ((p = strrchr(argv[0], '/')) != NULL) { @@ -221,7 +252,7 @@ int main(int argc, char* argv[]) { whoami = argv[0]; } - if (argc != 5) + if (argc < 5) { fprintf(stderr, "usage: %s n infile password outfile\n", whoami); exit(2); @@ -231,6 +262,7 @@ int main(int argc, char* argv[]) infile = argv[2]; password = argv[3]; outfile = argv[4]; + outfile2 = (argc > 5 ? argv[5] : 0); fn = ((n == 1) ? test01 : (n == 2) ? test02 : @@ -245,6 +277,7 @@ int main(int argc, char* argv[]) (n == 11) ? test11 : (n == 12) ? test12 : (n == 13) ? test13 : + (n == 14) ? test14 : 0); if (fn == 0) @@ -254,7 +287,7 @@ int main(int argc, char* argv[]) } qpdf = qpdf_init(); - fn(infile, password, outfile); + fn(infile, password, outfile, outfile2); qpdf_cleanup(&qpdf); assert(qpdf == 0); diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index cc65328f..85bbea1c 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -103,6 +103,8 @@ familiar with the PDF file format or who are PDF developers.\n\ --object-streams=mode controls handing of object streams\n\ --ignore-xref-streams tells qpdf to ignore any cross-reference streams\n\ --qdf turns on \"QDF mode\" (below)\n\ +--min-version=version sets the minimum PDF version of the output file\n\ +--force-version=version forces this to be the PDF version of the output file\n\ \n\ Values for stream data options:\n\ \n\ @@ -119,6 +121,12 @@ Values for object stream mode:\n\ In qdf mode, by default, content normalization is turned on, and the\n\ stream data mode is set to uncompress.\n\ \n\ +Setting the minimum PDF version of the output file may raise the version\n\ +but will never lower it. Forcing the PDF version of the output file may\n\ +set the PDF version to a lower value than actually allowed by the file's\n\ +contents. You should only do this if you have no other possible way to\n\ +open the file or if you know that the file definitely doesn't include\n\ +features not supported later versions.\n\ \n\ Testing, Inspection, and Debugging Options\n\ ------------------------------------------\n\ @@ -518,6 +526,8 @@ int main(int argc, char* argv[]) QPDFWriter::object_stream_e object_stream_mode = QPDFWriter::o_preserve; bool ignore_xref_streams = false; bool qdf_mode = false; + std::string min_version; + std::string force_version; bool static_id = false; bool suppress_original_object_id = false; @@ -651,6 +661,24 @@ int main(int argc, char* argv[]) { qdf_mode = true; } + else if (strcmp(arg, "min-version") == 0) + { + if (parameter == 0) + { + usage("--min-version be given as" + "--min-version=version"); + } + min_version = parameter; + } + else if (strcmp(arg, "force-version") == 0) + { + if (parameter == 0) + { + usage("--force-version be given as" + "--force-version=version"); + } + force_version = parameter; + } else if (strcmp(arg, "static-id") == 0) { static_id = true; @@ -977,6 +1005,14 @@ int main(int argc, char* argv[]) { w.setObjectStreamMode(object_stream_mode); } + if (! min_version.empty()) + { + w.setMinimumPDFVersion(min_version); + } + if (! force_version.empty()) + { + w.forcePDFVersion(force_version); + } w.write(); } if (! pdf.getWarnings().empty()) diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index cf4dc333..a2905e2a 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -153,3 +153,8 @@ qpdf-c called qpdf_allow_modify_form 0 qpdf-c called qpdf_allow_modify_annotation 0 qpdf-c called qpdf_allow_modify_other 0 qpdf-c called qpdf_allow_modify_all 0 +QPDFWriter increasing minimum version 0 +QPDFWriter using forced PDF version 0 +qpdf-c called qpdf_set_minimum_pdf_version 0 +qpdf-c called qpdf_force_pdf_version 0 +qpdf-c called qpdf_init_write multiple times 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 1c724ec3..1aba8e15 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -81,7 +81,7 @@ flush_tiff_cache(); show_ntests(); # ---------- $td->notify("--- Miscellaneous Tests ---"); -$n_tests += 7; +$n_tests += 14; foreach (my $i = 1; $i <= 3; ++$i) { @@ -115,6 +115,44 @@ $td->runtest("show new xref stream", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Min/Force version +$td->runtest("set min version", + {$td->COMMAND => "qpdf --min-version=1.6 good1.pdf a.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check version", + {$td->COMMAND => "qpdf --check a.pdf"}, + {$td->FILE => "min-version.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("force version", + {$td->COMMAND => "qpdf --force-version=1.4 a.pdf b.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check version", + {$td->COMMAND => "qpdf --check b.pdf"}, + {$td->FILE => "forced-version.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +unlink "a.pdf", "b.pdf" or die; +$td->runtest("C API: min/force versions", + {$td->COMMAND => "qpdf-ctest 14 object-stream.pdf '' a.pdf b.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("C check version 1", + {$td->COMMAND => "qpdf --check a.pdf"}, + {$td->FILE => "min-version.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("C check version 2", + {$td->COMMAND => "qpdf --check b.pdf"}, + {$td->FILE => "forced-version.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + show_ntests(); # ---------- $td->notify("--- Error Condition Tests ---"); @@ -883,7 +921,7 @@ foreach my $d (@cenc) my ($n, $infile, $pass, $description, $output) = @$d; my $outfile = $description; $outfile =~ s/ /-/g; - my $outfile = "c-$outfile.pdf"; + $outfile = "c-$outfile.pdf"; $td->runtest("C API encryption: $description", {$td->COMMAND => "qpdf-ctest $n $infile $pass a.pdf"}, {$td->STRING => $output, $td->EXIT_STATUS => 0}, diff --git a/qpdf/qtest/qpdf/forced-version.out b/qpdf/qtest/qpdf/forced-version.out new file mode 100644 index 00000000..9a71439d --- /dev/null +++ b/qpdf/qtest/qpdf/forced-version.out @@ -0,0 +1,5 @@ +checking b.pdf +PDF Version: 1.4 +File is not encrypted +File is not linearized +No errors found diff --git a/qpdf/qtest/qpdf/min-version.out b/qpdf/qtest/qpdf/min-version.out new file mode 100644 index 00000000..3e34d6a0 --- /dev/null +++ b/qpdf/qtest/qpdf/min-version.out @@ -0,0 +1,5 @@ +checking a.pdf +PDF Version: 1.6 +File is not encrypted +File is not linearized +No errors found