Add --replace-input option (fixes #321)

This commit is contained in:
Jay Berkenbilt 2019-08-31 15:11:11 -04:00
parent babd12c9b2
commit d492bb0a90
9 changed files with 186 additions and 18 deletions

View File

@ -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
@ -449,6 +451,21 @@ make
</para>
</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>
@ -4419,6 +4436,15 @@ print "\n";
CLI Enhancements
</para>
<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

View File

@ -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"
@ -2316,6 +2326,12 @@ ArgParser::argEndUnderOverlay()
o.under_overlay = 0;
}
void
ArgParser::argReplaceInput()
{
o.replace_input = true;
}
void
ArgParser::handleArgFileArguments()
{
@ -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);
}

View File

@ -189,6 +189,47 @@ foreach my $d (['auto-ü', 1], ['auto-öπ', 2])
$td->NORMALIZE_NEWLINES);
}
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 ---");
@ -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-*");
}

View File

@ -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

View File

@ -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

Binary file not shown.

View File

@ -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

Binary file not shown.

View File

@ -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