summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2019-08-31 21:11:11 +0200
committerJay Berkenbilt <ejb@ql.org>2019-08-31 21:51:21 +0200
commitd492bb0a90e30c8c57f36434479ddb708d322e79 (patch)
tree3e721d5622acb0fe6f74ab0889b317cde7daf7a2
parentbabd12c9b2b824416d2e5992d961396090e2bfea (diff)
downloadqpdf-d492bb0a90e30c8c57f36434479ddb708d322e79.tar.zst
Add --replace-input option (fixes #321)
-rw-r--r--manual/qpdf-manual.xml34
-rw-r--r--qpdf/qpdf.cc118
-rw-r--r--qpdf/qtest/qpdf.test43
-rw-r--r--qpdf/qtest/qpdf/bad-jpeg-show.out2
-rw-r--r--qpdf/qtest/qpdf/empty-object.out2
-rw-r--r--qpdf/qtest/qpdf/replace-input.pdfbin0 -> 743 bytes
-rw-r--r--qpdf/qtest/qpdf/replace-warn.out3
-rw-r--r--qpdf/qtest/qpdf/warn-replace.pdfbin0 -> 16504 bytes
-rw-r--r--qpdf/qtest/qpdf/xref-with-short-size.out2
9 files changed, 186 insertions, 18 deletions
diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml
index bfdefc41..01d1b9cf 100644
--- a/manual/qpdf-manual.xml
+++ b/manual/qpdf-manual.xml
@@ -331,10 +331,12 @@ make
<option>outfilename</option> does not have to be seekable, even
when generating linearized files. Specifying
&ldquo;<option>-</option>&rdquo; as <option>outfilename</option>
- means to write to standard output. However, you can't specify the
- same file as both the input and the output because qpdf reads data
- from the input file as it writes to the output file. QPDF attempts
- to detect this case and fail without overwriting the output file.
+ means to write to standard output. If you want to overwrite the
+ input file with the output, use the option
+ <option>--replace-input</option> and omit the output file name.
+ You can't specify the same file as both the input and the output.
+ If you do this, qpdf will tell you about the
+ <option>--replace-input</option> option.
</para>
<para>
Most options require an output file, but some testing or
@@ -450,6 +452,21 @@ make
</listitem>
</varlistentry>
<varlistentry>
+ <term><option>--replace-input</option></term>
+ <listitem>
+ <para>
+ If specified, the output file name should be omitted. This
+ option tells qpdf to replace the input file with the output.
+ It does this by writing to
+ <filename>.~qpdf-temp.<replaceable>infilename</replaceable>#</filename>
+ and, when done, overwriting the input file with the temporary
+ file. If there were any warnings, the original input is saved
+ as
+ <filename><replaceable>infilename</replaceable>.~qpdf-orig</filename>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><option>--copy-encryption=file</option></term>
<listitem>
<para>
@@ -4421,6 +4438,15 @@ print "\n";
<itemizedlist>
<listitem>
<para>
+ The <option>--replace-input</option> option may be given in
+ place of an output file name. This causes qpdf to overwrite
+ the input file with the output. See the description of
+ <option>--replace-input</option> in <xref
+ linkend="ref.basic-options"/> for more details.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
The <option>--recompress-flate</option> instructs
<command>qpdf</command> to recompress streams that are
already compressed with <literal>/FlateDecode</literal>.
diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc
index 4b28f73a..8bb1ce48 100644
--- a/qpdf/qpdf.cc
+++ b/qpdf/qpdf.cc
@@ -23,6 +23,7 @@
#include <qpdf/QPDFOutlineDocumentHelper.hh>
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
#include <qpdf/QPDFExc.hh>
+#include <qpdf/QPDFSystemError.hh>
#include <qpdf/QPDFWriter.hh>
#include <qpdf/QIntC.hh>
@@ -180,6 +181,7 @@ struct Options
overlay("overlay"),
under_overlay(0),
require_outfile(true),
+ replace_input(false),
infilename(0),
outfilename(0)
{
@@ -283,6 +285,7 @@ struct Options
std::vector<PageSpec> page_specs;
std::map<std::string, RotationSpec> rotations;
bool require_outfile;
+ bool replace_input;
char const* infilename;
char const* outfilename;
};
@@ -712,6 +715,7 @@ class ArgParser
void argUOrepeat(char* parameter);
void argUOpassword(char* parameter);
void argEndUnderOverlay();
+ void argReplaceInput();
void usage(std::string const& message);
void checkCompletion();
@@ -940,6 +944,7 @@ ArgParser::initOptionTable()
&ArgParser::argIiMinBytes, "minimum-bytes");
(*t)["overlay"] = oe_bare(&ArgParser::argOverlay);
(*t)["underlay"] = oe_bare(&ArgParser::argUnderlay);
+ (*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput);
t = &this->encrypt40_option_table;
(*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);
@@ -1080,6 +1085,9 @@ ArgParser::argHelp()
<< "will be interpreted as an argument. No interpolation is done. Line\n"
<< "terminators are stripped. @- can be specified to read from standard input.\n"
<< "\n"
+ << "The output file can be - to indicate writing to standard output, or it can\n"
+ << "be --replace-input to cause qpdf to replace the input file with the output.\n"
+ << "\n"
<< "Note that when contradictory options are provided, whichever options are\n"
<< "provided last take precedence.\n"
<< "\n"
@@ -1097,6 +1105,8 @@ ArgParser::argHelp()
<< "--progress give progress indicators while writing output\n"
<< "--no-warn suppress warnings\n"
<< "--linearize generated a linearized (web optimized) file\n"
+ << "--replace-input use in place of specifying an output file; qpdf will\n"
+ << " replace the input file with the output\n"
<< "--copy-encryption=file copy encryption parameters from specified file\n"
<< "--encryption-file-password=password\n"
<< " password used to open the file from which encryption\n"
@@ -2317,6 +2327,12 @@ ArgParser::argEndUnderOverlay()
}
void
+ArgParser::argReplaceInput()
+{
+ o.replace_input = true;
+}
+
+void
ArgParser::handleArgFileArguments()
{
// Support reading arguments from files. Create a new argv. Ensure
@@ -3048,15 +3064,28 @@ ArgParser::doFinalChecks()
{
usage("missing -- at end of options");
}
+ if (o.replace_input)
+ {
+ if (o.outfilename)
+ {
+ usage("--replace-input may not be used when"
+ " an output file is specified");
+ }
+ else if (o.split_pages)
+ {
+ usage("--split-pages may not be used with --replace-input");
+ }
+ }
if (o.infilename == 0)
{
usage("an input file name is required");
}
- else if (o.require_outfile && (o.outfilename == 0))
+ else if (o.require_outfile && (o.outfilename == 0) && (! o.replace_input))
{
usage("an output file name is required; use - for standard output");
}
- else if ((! o.require_outfile) && (o.outfilename != 0))
+ else if ((! o.require_outfile) &&
+ ((o.outfilename != 0) || o.replace_input))
{
usage("no output file may be given for this option");
}
@@ -3065,7 +3094,8 @@ ArgParser::doFinalChecks()
o.externalize_inline_images = true;
}
- if (o.require_outfile && (strcmp(o.outfilename, "-") == 0))
+ if (o.require_outfile && o.outfilename &&
+ (strcmp(o.outfilename, "-") == 0))
{
if (o.split_pages)
{
@@ -3088,7 +3118,7 @@ ArgParser::doFinalChecks()
{
QTC::TC("qpdf", "qpdf same file error");
usage("input file and output file are the same;"
- " this would cause input file to be lost");
+ " use --replace-input to intentionally overwrite the input file");
}
}
@@ -3861,6 +3891,12 @@ static void do_inspection(QPDF& pdf, Options& o)
{
do_show_pages(pdf, o);
}
+ if ((! pdf.getWarnings().empty()) && (exit_code != EXIT_ERROR))
+ {
+ std::cerr << whoami
+ << ": operation succeeded with warnings" << std::endl;
+ exit_code = EXIT_WARNING;
+ }
if (exit_code)
{
exit(exit_code);
@@ -5109,18 +5145,80 @@ static void do_split_pages(QPDF& pdf, Options& o)
static void write_outfile(QPDF& pdf, Options& o)
{
- if (strcmp(o.outfilename, "-") == 0)
+ std::string temp_out;
+ if (o.replace_input)
+ {
+ // Use a file name that is hidden by default in the OS to
+ // avoid having it become momentarily visible in a
+ // graphical file manager or in case it gets left behind
+ // because of some kind of error.
+ temp_out = ".~qpdf-temp." + std::string(o.infilename) + "#";
+ // o.outfilename will be restored to 0 before temp_out
+ // goes out of scope.
+ o.outfilename = temp_out.c_str();
+ }
+ else if (strcmp(o.outfilename, "-") == 0)
{
o.outfilename = 0;
}
- QPDFWriter w(pdf, o.outfilename);
- set_writer_options(pdf, o, w);
- w.write();
- if (o.verbose)
+ {
+ // Private scope so QPDFWriter will close the output file
+ QPDFWriter w(pdf, o.outfilename);
+ set_writer_options(pdf, o, w);
+ w.write();
+ }
+ if (o.verbose && o.outfilename)
{
std::cout << whoami << ": wrote file "
<< o.outfilename << std::endl;
}
+ if (o.replace_input)
+ {
+ o.outfilename = 0;
+ }
+ if (o.replace_input)
+ {
+ // We must close the input before we can rename files
+ pdf.closeInputSource();
+ std::string backup;
+ bool warnings = pdf.anyWarnings();
+ if (warnings)
+ {
+ // If there are warnings, the user may care about this
+ // file, so give it a non-hidden name that will be
+ // lexically grouped with the original file.
+ backup = std::string(o.infilename) + ".~qpdf-orig";
+ }
+ else
+ {
+ backup = ".~qpdf-orig." + std::string(o.infilename) + "#";
+ }
+ QUtil::rename_file(o.infilename, backup.c_str());
+ QUtil::rename_file(temp_out.c_str(), o.infilename);
+ if (warnings)
+ {
+ std::cerr << whoami
+ << ": there are warnings; original file kept in "
+ << backup << std::endl;
+ }
+ else
+ {
+ try
+ {
+ QUtil::remove_file(backup.c_str());
+ }
+ catch (QPDFSystemError& e)
+ {
+ std::cerr
+ << whoami
+ << ": unable to delete original file ("
+ << e.what() << ");"
+ << " original file left in " << backup
+ << ", but the input was successfully replaced"
+ << std::endl;
+ }
+ }
+ }
}
int realmain(int argc, char* argv[])
@@ -5156,7 +5254,7 @@ int realmain(int argc, char* argv[])
handle_under_overlay(pdf, o);
handle_transformations(pdf, o);
- if (o.outfilename == 0)
+ if ((o.outfilename == 0) && (! o.replace_input))
{
do_inspection(pdf, o);
}
diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test
index 41c94519..460685d9 100644
--- a/qpdf/qtest/qpdf.test
+++ b/qpdf/qtest/qpdf.test
@@ -191,6 +191,47 @@ foreach my $d (['auto-ü', 1], ['auto-öπ', 2])
show_ntests();
# ----------
+$td->notify("--- Replace Input ---");
+$n_tests += 8;
+
+# Use Unicode file names to test replace input so we can be sure it
+# works for that case.
+$td->runtest("create unicode filenames",
+ {$td->COMMAND => "test_unicode_filenames"},
+ {$td->STRING => "created Unicode filenames\n",
+ $td->EXIT_STATUS => 0},
+ $td->NORMALIZE_NEWLINES);
+
+foreach my $d (['auto-ü', 1], ['auto-öπ', 2])
+{
+ my ($u, $n) = @$d;
+ $td->runtest("replace input $u",
+ {$td->COMMAND => "qpdf --deterministic-id" .
+ " --object-streams=generate --replace-input $u.pdf"},
+ {$td->STRING => "", $td->EXIT_STATUS => 0},
+ $td->NORMALIZE_NEWLINES);
+ $td->runtest("check output ($u)",
+ {$td->FILE => "$u.pdf"},
+ {$td->FILE => "replace-input.pdf"},
+ $td->NORMALIZE_NEWLINES);
+}
+
+system("cp xref-with-short-size.pdf auto-warn.pdf") == 0 or die;
+$td->runtest("replace input with warnings",
+ {$td->COMMAND =>
+ "qpdf --deterministic-id --replace-input auto-warn.pdf"},
+ {$td->FILE => "replace-warn.out", $td->EXIT_STATUS => 3},
+ $td->NORMALIZE_NEWLINES);
+
+$td->runtest("check output",
+ {$td->FILE => "auto-warn.pdf"},
+ {$td->FILE => "warn-replace.pdf"});
+$td->runtest("check orig output",
+ {$td->FILE => "auto-warn.pdf.~qpdf-orig"},
+ {$td->FILE => "xref-with-short-size.pdf"});
+
+show_ntests();
+# ----------
$td->notify("--- Final Version ---");
$n_tests += 1;
@@ -4233,5 +4274,5 @@ sub get_md5_checksum
sub cleanup
{
system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache");
- system("rm -rf *split-out* ???-kfo.pdf *.tmpout \@file.pdf auto-*.pdf");
+ system("rm -rf *split-out* ???-kfo.pdf *.tmpout \@file.pdf auto-*");
}
diff --git a/qpdf/qtest/qpdf/bad-jpeg-show.out b/qpdf/qtest/qpdf/bad-jpeg-show.out
index f1b0bcc7..ca178e50 100644
--- a/qpdf/qtest/qpdf/bad-jpeg-show.out
+++ b/qpdf/qtest/qpdf/bad-jpeg-show.out
@@ -1,2 +1,2 @@
WARNING: bad-jpeg.pdf (offset 735): error decoding stream data for object 6 0: Not a JPEG file: starts with 0x77 0x77
-qpdf: operation succeeded with warnings; resulting file may have some problems
+qpdf: operation succeeded with warnings
diff --git a/qpdf/qtest/qpdf/empty-object.out b/qpdf/qtest/qpdf/empty-object.out
index 7ebfe52c..e2181c6e 100644
--- a/qpdf/qtest/qpdf/empty-object.out
+++ b/qpdf/qtest/qpdf/empty-object.out
@@ -1,3 +1,3 @@
WARNING: empty-object.pdf (object 7 0, offset 575): empty object treated as null
null
-qpdf: operation succeeded with warnings; resulting file may have some problems
+qpdf: operation succeeded with warnings
diff --git a/qpdf/qtest/qpdf/replace-input.pdf b/qpdf/qtest/qpdf/replace-input.pdf
new file mode 100644
index 00000000..b0a7e2e9
--- /dev/null
+++ b/qpdf/qtest/qpdf/replace-input.pdf
Binary files differ
diff --git a/qpdf/qtest/qpdf/replace-warn.out b/qpdf/qtest/qpdf/replace-warn.out
new file mode 100644
index 00000000..09a9261a
--- /dev/null
+++ b/qpdf/qtest/qpdf/replace-warn.out
@@ -0,0 +1,3 @@
+WARNING: auto-warn.pdf (xref stream, offset 16227): Cross-reference stream data has the wrong size; expected = 52; actual = 56
+qpdf: there are warnings; original file kept in auto-warn.pdf.~qpdf-orig
+qpdf: operation succeeded with warnings; resulting file may have some problems
diff --git a/qpdf/qtest/qpdf/warn-replace.pdf b/qpdf/qtest/qpdf/warn-replace.pdf
new file mode 100644
index 00000000..7d1bef39
--- /dev/null
+++ b/qpdf/qtest/qpdf/warn-replace.pdf
Binary files differ
diff --git a/qpdf/qtest/qpdf/xref-with-short-size.out b/qpdf/qtest/qpdf/xref-with-short-size.out
index 9041f854..9b4ef1ce 100644
--- a/qpdf/qtest/qpdf/xref-with-short-size.out
+++ b/qpdf/qtest/qpdf/xref-with-short-size.out
@@ -11,4 +11,4 @@ WARNING: xref-with-short-size.pdf (xref stream, offset 16227): Cross-reference s
10/0: compressed; stream = 5, index = 3
11/0: compressed; stream = 5, index = 7
12/0: compressed; stream = 5, index = 8
-qpdf: operation succeeded with warnings; resulting file may have some problems
+qpdf: operation succeeded with warnings