diff --git a/ChangeLog b/ChangeLog index 10f1adf..3a7b5a7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2025-02-02 Jay Berkenbilt + + * Have fix-qdf accept a second argument, interpreted as the output + file. Fixes #1330. + 2024-02-01 M Holger * Bug fix: in qpdf CLI / QPDFJob throw a QPDFUsage exception if a diff --git a/manual/fix-qdf.1.in b/manual/fix-qdf.1.in index 9cbd6fa..0c5d1a2 100644 --- a/manual/fix-qdf.1.in +++ b/manual/fix-qdf.1.in @@ -3,9 +3,11 @@ fix-qdf \- repair PDF files in QDF form after editing .SH SYNOPSIS .B fix-qdf -< \fIinfilename\fR > \fIoutfilename\fR +[\fIinfilename\fR [\fIoutfilename\fR]] .SH DESCRIPTION -The fix-qdf program is part of the qpdf package. +The fix-qdf program is part of the qpdf package. With no arguments, +fix-qdf reads from standard input and writes to standard output. With +one argument, it reads from that file and writes to standard output. .PP The fix-qdf program reads a PDF file in QDF form and writes out the same file with stream lengths, cross-reference table entries, and diff --git a/manual/qdf.rst b/manual/qdf.rst index d1bf4b6..96cfc28 100644 --- a/manual/qdf.rst +++ b/manual/qdf.rst @@ -23,10 +23,18 @@ files are full of offset and length information that makes it hard to add or remove data. A QDF file is organized in a manner such that, if edits are kept within certain constraints, the :command:`fix-qdf` program, distributed with qpdf, is -able to restore edited files to a correct state. The -:command:`fix-qdf` program takes no command-line -arguments. It reads a possibly edited QDF file from standard input and -writes a repaired file to standard output. +able to restore edited files to a correct state. + +.. code-block:: bash + + fix-qdf [infilename [outfilename]] + +With no arguments, :command:`fix-qdf` reads the possibly-edited QDF +file from standard input and writes a repaired file to standard +output. You can also specify the input and output files as +command-line arguments. With one argument, the argument is taken as an +input file. With two arguments, the first argument is an input file, +and the second is an output file. For another way to work with PDF files in an editor, see :ref:`json`. Using qpdf JSON format allows you to edit the PDF file semantically diff --git a/manual/release-notes.rst b/manual/release-notes.rst index 76faa39..26fc535 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -38,6 +38,14 @@ Planned changes for future 12.x (subject to change): .. x.y.z: not yet released +11.10.0: not yet released + - CLI Enhancements + + - The :command:`fix-qdf` command now allows an output file to be + specified as an optional second argument. This is useful for + environments in which writing a binary file to standard output + doesn't work (such as PowerShell 5). + 11.9.1: June 7, 2024 - Bug Fixes diff --git a/qpdf/fix-qdf.cc b/qpdf/fix-qdf.cc index 510bb46..1770ddd 100644 --- a/qpdf/fix-qdf.cc +++ b/qpdf/fix-qdf.cc @@ -4,23 +4,27 @@ #include #include #include +#include #include #include #include +using namespace std::literals; static char const* whoami = nullptr; static void usage() { - std::cerr << "Usage: " << whoami << " [filename]" << std::endl; - exit(2); + std::cerr << "Usage: " << whoami << " [infilename [outfilename]]" << std::endl + << "infilename defaults to standard output" << std::endl + << "outfilename defaults to standard output" << std::endl; } class QdfFixer { public: - QdfFixer(std::string const& filename); + QdfFixer(std::string const& filename, std::ostream& out); + ~QdfFixer() = default; void processLines(std::string const& input); private: @@ -31,6 +35,7 @@ class QdfFixer void writeBinary(unsigned long long val, size_t bytes); std::string filename; + std::ostream& out; enum { st_top, st_in_obj, @@ -67,8 +72,9 @@ class QdfFixer std::string ostream_extends; }; -QdfFixer::QdfFixer(std::string const& filename) : - filename(filename) +QdfFixer::QdfFixer(std::string const& filename, std::ostream& out) : + filename(filename), + out(out) { } @@ -131,9 +137,9 @@ QdfFixer::processLines(std::string const& input) xref_offset = last_offset; state = st_at_xref; } - std::cout << line; + out << line; } else if (state == st_in_obj) { - std::cout << line; + out << line; if (line.compare("stream\n"sv) == 0) { state = st_in_stream; stream_start = offset; @@ -166,8 +172,8 @@ QdfFixer::processLines(std::string const& input) auto esize = 1 + xref_f1_nbytes + xref_f2_nbytes; xref_size = 1 + xref.size(); auto length = xref_size * esize; - std::cout << " /Length " << length << "\n" - << " /W [ 1 " << xref_f1_nbytes << " " << xref_f2_nbytes << " ]\n"; + out << " /Length " << length << "\n" + << " /W [ 1 " << xref_f1_nbytes << " " << xref_f2_nbytes << " ]\n"; state = st_in_xref_stream_dict; } } else if (state == st_in_ostream_dict) { @@ -209,10 +215,10 @@ QdfFixer::processLines(std::string const& input) if ((line.find("/Length"sv) != line.npos) || (line.find("/W"sv) != line.npos)) { // already printed } else if (line.find("/Size"sv) != line.npos) { - auto xref_size = 1 + xref.size(); - std::cout << " /Size " << xref_size << "\n"; + auto size = 1 + xref.size(); + out << " /Size " << size << "\n"; } else { - std::cout << line; + out << line; } if (line.compare("stream\n"sv) == 0) { writeBinary(0, 1); @@ -232,9 +238,9 @@ QdfFixer::processLines(std::string const& input) writeBinary(f1, xref_f1_nbytes); writeBinary(f2, xref_f2_nbytes); } - std::cout << "\nendstream\nendobj\n\n" - << "startxref\n" - << xref_offset << "\n%%EOF\n"; + out << "\nendstream\nendobj\n\n" + << "startxref\n" + << xref_offset << "\n%%EOF\n"; state = st_done; } } else if (state == st_in_stream) { @@ -242,7 +248,7 @@ QdfFixer::processLines(std::string const& input) stream_length = QIntC::to_size(last_offset - stream_start); state = st_after_stream; } - std::cout << line; + out << line; } else if (state == st_after_stream) { if (line.compare("%QDF: ignore_newline\n"sv) == 0) { if (stream_length > 0) { @@ -252,7 +258,7 @@ QdfFixer::processLines(std::string const& input) checkObjId(m[1].str()); state = st_in_length; } - std::cout << line; + out << line; } else if (state == st_in_length) { if (!matches(re_num)) { fatal(filename + ":" + std::to_string(lineno) + ": expected integer"); @@ -260,29 +266,29 @@ QdfFixer::processLines(std::string const& input) std::string new_length = std::to_string(stream_length) + "\n"; offset -= QIntC::to_offset(line.length()); offset += QIntC::to_offset(new_length.length()); - std::cout << new_length; + out << new_length; state = st_top; } else if (state == st_at_xref) { auto n = xref.size(); - std::cout << "0 " << 1 + n << "\n0000000000 65535 f \n"; + out << "0 " << 1 + n << "\n0000000000 65535 f \n"; for (auto const& e: xref) { - std::cout << QUtil::int_to_string(e.getOffset(), 10) << " 00000 n \n"; + out << QUtil::int_to_string(e.getOffset(), 10) << " 00000 n \n"; } state = st_before_trailer; } else if (state == st_before_trailer) { if (line.compare("trailer <<\n"sv) == 0) { - std::cout << line; + out << line; state = st_in_trailer; } // no output } else if (state == st_in_trailer) { if (matches(re_size_n)) { - std::cout << " /Size " << 1 + xref.size() << "\n"; + out << " /Size " << 1 + xref.size() << "\n"; } else { - std::cout << line; + out << line; } if (line.compare(">>\n"sv) == 0) { - std::cout << "startxref\n" << xref_offset << "\n%%EOF\n"; + out << "startxref\n" << xref_offset << "\n%%EOF\n"; state = st_done; } } else if (state == st_done) { @@ -332,9 +338,9 @@ QdfFixer::writeOstream() } dict_data += ">>\n"; offset_adjust += QIntC::to_offset(dict_data.length()); - std::cout << dict_data << "stream\n" << offsets; + out << dict_data << "stream\n" << offsets; for (auto const& o: ostream) { - std::cout << o; + out << o; } for (auto const& o: ostream_discarded) { @@ -361,7 +367,7 @@ QdfFixer::writeBinary(unsigned long long val, size_t bytes) data[i - 1] = static_cast(val & 0xff); // i.e. val % 256 val >>= 8; // i.e. val = val / 256 } - std::cout << data; + out << data; } static int @@ -370,27 +376,44 @@ realmain(int argc, char* argv[]) whoami = QUtil::getWhoami(argv[0]); QUtil::setLineBuf(stdout); char const* filename = nullptr; - if (argc > 2) { + char const* outfilename = nullptr; + if (argc > 3) { usage(); } else if ((argc > 1) && (strcmp(argv[1], "--version") == 0)) { std::cout << whoami << " from qpdf version " << QPDF::QPDFVersion() << std::endl; return 0; } else if ((argc > 1) && (strcmp(argv[1], "--help") == 0)) { usage(); - } else if (argc == 2) { + } else if (argc >= 2) { filename = argv[1]; + if (argc == 3) { + outfilename = argv[2]; + } } - std::string input; - if (filename == nullptr) { - filename = "standard input"; - QUtil::binary_stdin(); - input = QUtil::read_file_into_string(stdin); - } else { - input = QUtil::read_file_into_string(filename); + try { + std::string input; + if (filename == nullptr) { + filename = "standard input"; + QUtil::binary_stdin(); + input = QUtil::read_file_into_string(stdin); + } else { + input = QUtil::read_file_into_string(filename); + } + std::unique_ptr out = nullptr; + if (outfilename) { + out = std::make_unique(outfilename, std::ios::binary); + if (out->fail()) { + QUtil::throw_system_error("open "s + outfilename); + } + } else { + QUtil::binary_stdout(); + } + QdfFixer qf(filename, out ? *out : std::cout); + qf.processLines(input); + } catch (std::exception& e) { + std::cerr << whoami << ": error: " << e.what() << std::endl; + exit(qpdf_exit_error); } - QUtil::binary_stdout(); - QdfFixer qf(filename); - qf.processLines(input); return 0; } diff --git a/qpdf/qtest/fix-qdf.test b/qpdf/qtest/fix-qdf.test index 96f4236..af5f565 100644 --- a/qpdf/qtest/fix-qdf.test +++ b/qpdf/qtest/fix-qdf.test @@ -14,7 +14,7 @@ cleanup(); my $td = new TestDriver('fix-qdf'); -my $n_tests = 5; +my $n_tests = 11; for (my $n = 1; $n <= 2; ++$n) { @@ -23,6 +23,15 @@ for (my $n = 1; $n <= 2; ++$n) {$td->FILE => "fix$n.qdf.out", $td->EXIT_STATUS => 0}); + $td->runtest("fix-qdf $n with named output", + {$td->COMMAND => "fix-qdf fix$n.qdf a.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}); + + $td->runtest("check fix-qdf $n output", + {$td->FILE => "a.pdf"}, + {$td->FILE => "fix$n.qdf.out"}); + $td->runtest("identity fix-qdf $n", {$td->COMMAND => "fix-qdf fix$n.qdf.out"}, {$td->FILE => "fix$n.qdf.out", @@ -54,5 +63,15 @@ $td->runtest("fix-qdf with big object stream", # > 255 objects in a stream {$td->FILE => "big-ostream.pdf", $td->EXIT_STATUS => 0}); +$td->runtest("fix-qdf error opening input", + {$td->COMMAND => "fix-qdf /does/not/exist/potato.pdf"}, + {$td->REGEXP => "^fix-qdf: error: open .*/does/not/exist/potato.pdf: .*", + $td->EXIT_STATUS => 2}); + +$td->runtest("fix-qdf error opening output", # > 255 objects in a stream + {$td->COMMAND => "fix-qdf fix1.qdf /does/not/exist/salad.pdf"}, + {$td->REGEXP => "^fix-qdf: error: open .*/does/not/exist/salad.pdf: .*", + $td->EXIT_STATUS => 2}); + cleanup(); $td->report($n_tests);