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 does not have to be seekable, even when generating linearized files. Specifying “” as - 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 + 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. Most options require an output file, but some testing or @@ -449,6 +451,21 @@ make + + + + + 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 + .~qpdf-temp.infilename# + and, when done, overwriting the input file with the temporary + file. If there were any warnings, the original input is saved + as + infilename.~qpdf-orig. + + + @@ -4419,6 +4436,15 @@ print "\n"; CLI Enhancements + + + The 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 + in for more details. + + The instructs 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 #include #include +#include #include #include @@ -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 page_specs; std::map 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); } 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 @@ -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-*"); } 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 Binary files /dev/null and b/qpdf/qtest/qpdf/replace-input.pdf 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 Binary files /dev/null and b/qpdf/qtest/qpdf/warn-replace.pdf 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