2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-09-28 04:59:05 +00:00

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 <option>outfilename</option> does not have to be seekable, even
when generating linearized files. Specifying when generating linearized files. Specifying
&ldquo;<option>-</option>&rdquo; as <option>outfilename</option> &ldquo;<option>-</option>&rdquo; as <option>outfilename</option>
means to write to standard output. However, you can't specify the means to write to standard output. If you want to overwrite the
same file as both the input and the output because qpdf reads data input file with the output, use the option
from the input file as it writes to the output file. QPDF attempts <option>--replace-input</option> and omit the output file name.
to detect this case and fail without overwriting the output file. 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>
<para> <para>
Most options require an output file, but some testing or Most options require an output file, but some testing or
@ -449,6 +451,21 @@ make
</para> </para>
</listitem> </listitem>
</varlistentry> </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> <varlistentry>
<term><option>--copy-encryption=file</option></term> <term><option>--copy-encryption=file</option></term>
<listitem> <listitem>
@ -4419,6 +4436,15 @@ print "\n";
CLI Enhancements CLI Enhancements
</para> </para>
<itemizedlist> <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> <listitem>
<para> <para>
The <option>--recompress-flate</option> instructs The <option>--recompress-flate</option> instructs

View File

@ -23,6 +23,7 @@
#include <qpdf/QPDFOutlineDocumentHelper.hh> #include <qpdf/QPDFOutlineDocumentHelper.hh>
#include <qpdf/QPDFAcroFormDocumentHelper.hh> #include <qpdf/QPDFAcroFormDocumentHelper.hh>
#include <qpdf/QPDFExc.hh> #include <qpdf/QPDFExc.hh>
#include <qpdf/QPDFSystemError.hh>
#include <qpdf/QPDFWriter.hh> #include <qpdf/QPDFWriter.hh>
#include <qpdf/QIntC.hh> #include <qpdf/QIntC.hh>
@ -180,6 +181,7 @@ struct Options
overlay("overlay"), overlay("overlay"),
under_overlay(0), under_overlay(0),
require_outfile(true), require_outfile(true),
replace_input(false),
infilename(0), infilename(0),
outfilename(0) outfilename(0)
{ {
@ -283,6 +285,7 @@ struct Options
std::vector<PageSpec> page_specs; std::vector<PageSpec> page_specs;
std::map<std::string, RotationSpec> rotations; std::map<std::string, RotationSpec> rotations;
bool require_outfile; bool require_outfile;
bool replace_input;
char const* infilename; char const* infilename;
char const* outfilename; char const* outfilename;
}; };
@ -712,6 +715,7 @@ class ArgParser
void argUOrepeat(char* parameter); void argUOrepeat(char* parameter);
void argUOpassword(char* parameter); void argUOpassword(char* parameter);
void argEndUnderOverlay(); void argEndUnderOverlay();
void argReplaceInput();
void usage(std::string const& message); void usage(std::string const& message);
void checkCompletion(); void checkCompletion();
@ -940,6 +944,7 @@ ArgParser::initOptionTable()
&ArgParser::argIiMinBytes, "minimum-bytes"); &ArgParser::argIiMinBytes, "minimum-bytes");
(*t)["overlay"] = oe_bare(&ArgParser::argOverlay); (*t)["overlay"] = oe_bare(&ArgParser::argOverlay);
(*t)["underlay"] = oe_bare(&ArgParser::argUnderlay); (*t)["underlay"] = oe_bare(&ArgParser::argUnderlay);
(*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput);
t = &this->encrypt40_option_table; t = &this->encrypt40_option_table;
(*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);
@ -1080,6 +1085,9 @@ ArgParser::argHelp()
<< "will be interpreted as an argument. No interpolation is done. Line\n" << "will be interpreted as an argument. No interpolation is done. Line\n"
<< "terminators are stripped. @- can be specified to read from standard input.\n" << "terminators are stripped. @- can be specified to read from standard input.\n"
<< "\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" << "Note that when contradictory options are provided, whichever options are\n"
<< "provided last take precedence.\n" << "provided last take precedence.\n"
<< "\n" << "\n"
@ -1097,6 +1105,8 @@ ArgParser::argHelp()
<< "--progress give progress indicators while writing output\n" << "--progress give progress indicators while writing output\n"
<< "--no-warn suppress warnings\n" << "--no-warn suppress warnings\n"
<< "--linearize generated a linearized (web optimized) file\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" << "--copy-encryption=file copy encryption parameters from specified file\n"
<< "--encryption-file-password=password\n" << "--encryption-file-password=password\n"
<< " password used to open the file from which encryption\n" << " password used to open the file from which encryption\n"
@ -2316,6 +2326,12 @@ ArgParser::argEndUnderOverlay()
o.under_overlay = 0; o.under_overlay = 0;
} }
void
ArgParser::argReplaceInput()
{
o.replace_input = true;
}
void void
ArgParser::handleArgFileArguments() ArgParser::handleArgFileArguments()
{ {
@ -3048,15 +3064,28 @@ ArgParser::doFinalChecks()
{ {
usage("missing -- at end of options"); 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) if (o.infilename == 0)
{ {
usage("an input file name is required"); 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"); 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"); usage("no output file may be given for this option");
} }
@ -3065,7 +3094,8 @@ ArgParser::doFinalChecks()
o.externalize_inline_images = true; 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) if (o.split_pages)
{ {
@ -3088,7 +3118,7 @@ ArgParser::doFinalChecks()
{ {
QTC::TC("qpdf", "qpdf same file error"); QTC::TC("qpdf", "qpdf same file error");
usage("input file and output file are the same;" 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); 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) if (exit_code)
{ {
exit(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) 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; o.outfilename = 0;
} }
QPDFWriter w(pdf, o.outfilename); {
set_writer_options(pdf, o, w); // Private scope so QPDFWriter will close the output file
w.write(); QPDFWriter w(pdf, o.outfilename);
if (o.verbose) set_writer_options(pdf, o, w);
w.write();
}
if (o.verbose && o.outfilename)
{ {
std::cout << whoami << ": wrote file " std::cout << whoami << ": wrote file "
<< o.outfilename << std::endl; << 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[]) int realmain(int argc, char* argv[])
@ -5156,7 +5254,7 @@ int realmain(int argc, char* argv[])
handle_under_overlay(pdf, o); handle_under_overlay(pdf, o);
handle_transformations(pdf, o); handle_transformations(pdf, o);
if (o.outfilename == 0) if ((o.outfilename == 0) && (! o.replace_input))
{ {
do_inspection(pdf, o); do_inspection(pdf, o);
} }

View File

@ -189,6 +189,47 @@ foreach my $d (['auto-ü', 1], ['auto-öπ', 2])
$td->NORMALIZE_NEWLINES); $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(); show_ntests();
# ---------- # ----------
$td->notify("--- Final Version ---"); $td->notify("--- Final Version ---");
@ -4233,5 +4274,5 @@ sub get_md5_checksum
sub cleanup sub cleanup
{ {
system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache"); 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 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 WARNING: empty-object.pdf (object 7 0, offset 575): empty object treated as null
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 10/0: compressed; stream = 5, index = 3
11/0: compressed; stream = 5, index = 7 11/0: compressed; stream = 5, index = 7
12/0: compressed; stream = 5, index = 8 12/0: compressed; stream = 5, index = 8
qpdf: operation succeeded with warnings; resulting file may have some problems qpdf: operation succeeded with warnings