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 2024-02-01 M Holger <m.holger@qpdf.org> 6 2024-02-01 M Holger <m.holger@qpdf.org>
2 7
3 * Bug fix: in qpdf CLI / QPDFJob throw a QPDFUsage exception if a 8 * Bug fix: in qpdf CLI / QPDFJob throw a QPDFUsage exception if a
manual/fix-qdf.1.in
@@ -3,9 +3,11 @@ @@ -3,9 +3,11 @@
3 fix-qdf \- repair PDF files in QDF form after editing 3 fix-qdf \- repair PDF files in QDF form after editing
4 .SH SYNOPSIS 4 .SH SYNOPSIS
5 .B fix-qdf 5 .B fix-qdf
6 -< \fIinfilename\fR > \fIoutfilename\fR 6 +[\fIinfilename\fR [\fIoutfilename\fR]]
7 .SH DESCRIPTION 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 .PP 11 .PP
10 The fix-qdf program reads a PDF file in QDF form and writes out 12 The fix-qdf program reads a PDF file in QDF form and writes out
11 the same file with stream lengths, cross-reference table entries, and 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,10 +23,18 @@ files are full of offset and length information that makes it hard to
23 add or remove data. A QDF file is organized in a manner such that, if 23 add or remove data. A QDF file is organized in a manner such that, if
24 edits are kept within certain constraints, the 24 edits are kept within certain constraints, the
25 :command:`fix-qdf` program, distributed with qpdf, is 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 For another way to work with PDF files in an editor, see :ref:`json`. 39 For another way to work with PDF files in an editor, see :ref:`json`.
32 Using qpdf JSON format allows you to edit the PDF file semantically 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,6 +38,14 @@ Planned changes for future 12.x (subject to change):
38 38
39 .. x.y.z: not yet released 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 11.9.1: June 7, 2024 49 11.9.1: June 7, 2024
42 - Bug Fixes 50 - Bug Fixes
43 51
qpdf/fix-qdf.cc
@@ -4,17 +4,20 @@ @@ -4,17 +4,20 @@
4 #include <qpdf/QUtil.hh> 4 #include <qpdf/QUtil.hh>
5 #include <cstdio> 5 #include <cstdio>
6 #include <cstring> 6 #include <cstring>
  7 +#include <fstream>
7 #include <iostream> 8 #include <iostream>
8 #include <regex> 9 #include <regex>
9 #include <string_view> 10 #include <string_view>
10 11
  12 +using namespace std::literals;
11 static char const* whoami = nullptr; 13 static char const* whoami = nullptr;
12 14
13 static void 15 static void
14 usage() 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 class QdfFixer 23 class QdfFixer
@@ -373,27 +376,44 @@ realmain(int argc, char* argv[]) @@ -373,27 +376,44 @@ realmain(int argc, char* argv[])
373 whoami = QUtil::getWhoami(argv[0]); 376 whoami = QUtil::getWhoami(argv[0]);
374 QUtil::setLineBuf(stdout); 377 QUtil::setLineBuf(stdout);
375 char const* filename = nullptr; 378 char const* filename = nullptr;
376 - if (argc > 2) { 379 + char const* outfilename = nullptr;
  380 + if (argc > 3) {
377 usage(); 381 usage();
378 } else if ((argc > 1) && (strcmp(argv[1], "--version") == 0)) { 382 } else if ((argc > 1) && (strcmp(argv[1], "--version") == 0)) {
379 std::cout << whoami << " from qpdf version " << QPDF::QPDFVersion() << std::endl; 383 std::cout << whoami << " from qpdf version " << QPDF::QPDFVersion() << std::endl;
380 return 0; 384 return 0;
381 } else if ((argc > 1) && (strcmp(argv[1], "--help") == 0)) { 385 } else if ((argc > 1) && (strcmp(argv[1], "--help") == 0)) {
382 usage(); 386 usage();
383 - } else if (argc == 2) { 387 + } else if (argc >= 2) {
384 filename = argv[1]; 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 return 0; 417 return 0;
398 } 418 }
399 419
qpdf/qtest/fix-qdf.test
@@ -14,7 +14,7 @@ cleanup(); @@ -14,7 +14,7 @@ cleanup();
14 14
15 my $td = new TestDriver('fix-qdf'); 15 my $td = new TestDriver('fix-qdf');
16 16
17 -my $n_tests = 5; 17 +my $n_tests = 11;
18 18
19 for (my $n = 1; $n <= 2; ++$n) 19 for (my $n = 1; $n <= 2; ++$n)
20 { 20 {
@@ -23,6 +23,15 @@ for (my $n = 1; $n &lt;= 2; ++$n) @@ -23,6 +23,15 @@ for (my $n = 1; $n &lt;= 2; ++$n)
23 {$td->FILE => "fix$n.qdf.out", 23 {$td->FILE => "fix$n.qdf.out",
24 $td->EXIT_STATUS => 0}); 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 $td->runtest("identity fix-qdf $n", 35 $td->runtest("identity fix-qdf $n",
27 {$td->COMMAND => "fix-qdf fix$n.qdf.out"}, 36 {$td->COMMAND => "fix-qdf fix$n.qdf.out"},
28 {$td->FILE => "fix$n.qdf.out", 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,5 +63,15 @@ $td-&gt;runtest(&quot;fix-qdf with big object stream&quot;, # &gt; 255 objects in a stream
54 {$td->FILE => "big-ostream.pdf", 63 {$td->FILE => "big-ostream.pdf",
55 $td->EXIT_STATUS => 0}); 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 cleanup(); 76 cleanup();
58 $td->report($n_tests); 77 $td->report($n_tests);