Commit a2fc5b522e2bacf0eb0cc165388a63a19f7e1fdc

Authored by Jay Berkenbilt
1 parent 9cf96201

fix-qdf: accept optional output file (fixes #1330)

ChangeLog
  1 +2025-02-02 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Have fix-qdf accept a second argument, interpreted as the output
  4 + file. Fixes #1330.
  5 +
1 6 2024-02-01 M Holger <m.holger@qpdf.org>
2 7  
3 8 * Bug fix: in qpdf CLI / QPDFJob throw a QPDFUsage exception if a
... ...
manual/fix-qdf.1.in
... ... @@ -3,9 +3,11 @@
3 3 fix-qdf \- repair PDF files in QDF form after editing
4 4 .SH SYNOPSIS
5 5 .B fix-qdf
6   -< \fIinfilename\fR > \fIoutfilename\fR
  6 +[\fIinfilename\fR [\fIoutfilename\fR]]
7 7 .SH DESCRIPTION
8   -The fix-qdf program is part of the qpdf package.
  8 +The fix-qdf program is part of the qpdf package. With no arguments,
  9 +fix-qdf reads from standard input and writes to standard output. With
  10 +one argument, it reads from that file and writes to standard output.
9 11 .PP
10 12 The fix-qdf program reads a PDF file in QDF form and writes out
11 13 the same file with stream lengths, cross-reference table entries, and
... ...
manual/qdf.rst
... ... @@ -23,10 +23,18 @@ files are full of offset and length information that makes it hard to
23 23 add or remove data. A QDF file is organized in a manner such that, if
24 24 edits are kept within certain constraints, the
25 25 :command:`fix-qdf` program, distributed with qpdf, is
26   -able to restore edited files to a correct state. The
27   -:command:`fix-qdf` program takes no command-line
28   -arguments. It reads a possibly edited QDF file from standard input and
29   -writes a repaired file to standard output.
  26 +able to restore edited files to a correct state.
  27 +
  28 +.. code-block:: bash
  29 +
  30 + fix-qdf [infilename [outfilename]]
  31 +
  32 +With no arguments, :command:`fix-qdf` reads the possibly-edited QDF
  33 +file from standard input and writes a repaired file to standard
  34 +output. You can also specify the input and output files as
  35 +command-line arguments. With one argument, the argument is taken as an
  36 +input file. With two arguments, the first argument is an input file,
  37 +and the second is an output file.
30 38  
31 39 For another way to work with PDF files in an editor, see :ref:`json`.
32 40 Using qpdf JSON format allows you to edit the PDF file semantically
... ...
manual/release-notes.rst
... ... @@ -38,6 +38,14 @@ Planned changes for future 12.x (subject to change):
38 38  
39 39 .. x.y.z: not yet released
40 40  
  41 +11.10.0: not yet released
  42 + - CLI Enhancements
  43 +
  44 + - The :command:`fix-qdf` command now allows an output file to be
  45 + specified as an optional second argument. This is useful for
  46 + environments in which writing a binary file to standard output
  47 + doesn't work (such as PowerShell 5).
  48 +
41 49 11.9.1: June 7, 2024
42 50 - Bug Fixes
43 51  
... ...
qpdf/fix-qdf.cc
... ... @@ -4,17 +4,20 @@
4 4 #include <qpdf/QUtil.hh>
5 5 #include <cstdio>
6 6 #include <cstring>
  7 +#include <fstream>
7 8 #include <iostream>
8 9 #include <regex>
9 10 #include <string_view>
10 11  
  12 +using namespace std::literals;
11 13 static char const* whoami = nullptr;
12 14  
13 15 static void
14 16 usage()
15 17 {
16   - std::cerr << "Usage: " << whoami << " [filename]" << std::endl;
17   - exit(2);
  18 + std::cerr << "Usage: " << whoami << " [infilename [outfilename]]" << std::endl
  19 + << "infilename defaults to standard output" << std::endl
  20 + << "outfilename defaults to standard output" << std::endl;
18 21 }
19 22  
20 23 class QdfFixer
... ... @@ -373,27 +376,44 @@ realmain(int argc, char* argv[])
373 376 whoami = QUtil::getWhoami(argv[0]);
374 377 QUtil::setLineBuf(stdout);
375 378 char const* filename = nullptr;
376   - if (argc > 2) {
  379 + char const* outfilename = nullptr;
  380 + if (argc > 3) {
377 381 usage();
378 382 } else if ((argc > 1) && (strcmp(argv[1], "--version") == 0)) {
379 383 std::cout << whoami << " from qpdf version " << QPDF::QPDFVersion() << std::endl;
380 384 return 0;
381 385 } else if ((argc > 1) && (strcmp(argv[1], "--help") == 0)) {
382 386 usage();
383   - } else if (argc == 2) {
  387 + } else if (argc >= 2) {
384 388 filename = argv[1];
  389 + if (argc == 3) {
  390 + outfilename = argv[2];
  391 + }
385 392 }
386   - std::string input;
387   - if (filename == nullptr) {
388   - filename = "standard input";
389   - QUtil::binary_stdin();
390   - input = QUtil::read_file_into_string(stdin);
391   - } else {
392   - input = QUtil::read_file_into_string(filename);
  393 + try {
  394 + std::string input;
  395 + if (filename == nullptr) {
  396 + filename = "standard input";
  397 + QUtil::binary_stdin();
  398 + input = QUtil::read_file_into_string(stdin);
  399 + } else {
  400 + input = QUtil::read_file_into_string(filename);
  401 + }
  402 + std::unique_ptr<std::ofstream> out = nullptr;
  403 + if (outfilename) {
  404 + out = std::make_unique<std::ofstream>(outfilename, std::ios::binary);
  405 + if (out->fail()) {
  406 + QUtil::throw_system_error("open "s + outfilename);
  407 + }
  408 + } else {
  409 + QUtil::binary_stdout();
  410 + }
  411 + QdfFixer qf(filename, out ? *out : std::cout);
  412 + qf.processLines(input);
  413 + } catch (std::exception& e) {
  414 + std::cerr << whoami << ": error: " << e.what() << std::endl;
  415 + exit(qpdf_exit_error);
393 416 }
394   - QUtil::binary_stdout();
395   - QdfFixer qf(filename, std::cout);
396   - qf.processLines(input);
397 417 return 0;
398 418 }
399 419  
... ...
qpdf/qtest/fix-qdf.test
... ... @@ -14,7 +14,7 @@ cleanup();
14 14  
15 15 my $td = new TestDriver('fix-qdf');
16 16  
17   -my $n_tests = 5;
  17 +my $n_tests = 11;
18 18  
19 19 for (my $n = 1; $n <= 2; ++$n)
20 20 {
... ... @@ -23,6 +23,15 @@ for (my $n = 1; $n &lt;= 2; ++$n)
23 23 {$td->FILE => "fix$n.qdf.out",
24 24 $td->EXIT_STATUS => 0});
25 25  
  26 + $td->runtest("fix-qdf $n with named output",
  27 + {$td->COMMAND => "fix-qdf fix$n.qdf a.pdf"},
  28 + {$td->STRING => "",
  29 + $td->EXIT_STATUS => 0});
  30 +
  31 + $td->runtest("check fix-qdf $n output",
  32 + {$td->FILE => "a.pdf"},
  33 + {$td->FILE => "fix$n.qdf.out"});
  34 +
26 35 $td->runtest("identity fix-qdf $n",
27 36 {$td->COMMAND => "fix-qdf fix$n.qdf.out"},
28 37 {$td->FILE => "fix$n.qdf.out",
... ... @@ -54,5 +63,15 @@ $td-&gt;runtest(&quot;fix-qdf with big object stream&quot;, # &gt; 255 objects in a stream
54 63 {$td->FILE => "big-ostream.pdf",
55 64 $td->EXIT_STATUS => 0});
56 65  
  66 +$td->runtest("fix-qdf error opening input",
  67 + {$td->COMMAND => "fix-qdf /does/not/exist/potato.pdf"},
  68 + {$td->REGEXP => "^fix-qdf: error: open .*/does/not/exist/potato.pdf: .*",
  69 + $td->EXIT_STATUS => 2});
  70 +
  71 +$td->runtest("fix-qdf error opening output", # > 255 objects in a stream
  72 + {$td->COMMAND => "fix-qdf fix1.qdf /does/not/exist/salad.pdf"},
  73 + {$td->REGEXP => "^fix-qdf: error: open .*/does/not/exist/salad.pdf: .*",
  74 + $td->EXIT_STATUS => 2});
  75 +
57 76 cleanup();
58 77 $td->report($n_tests);
... ...