Commit d492bb0a90e30c8c57f36434479ddb708d322e79
1 parent
babd12c9
Add --replace-input option (fixes #321)
Showing
9 changed files
with
186 additions
and
18 deletions
manual/qpdf-manual.xml
| ... | ... | @@ -331,10 +331,12 @@ make |
| 331 | 331 | <option>outfilename</option> does not have to be seekable, even |
| 332 | 332 | when generating linearized files. Specifying |
| 333 | 333 | “<option>-</option>” as <option>outfilename</option> |
| 334 | - means to write to standard output. However, you can't specify the | |
| 335 | - same file as both the input and the output because qpdf reads data | |
| 336 | - from the input file as it writes to the output file. QPDF attempts | |
| 337 | - to detect this case and fail without overwriting the output file. | |
| 334 | + means to write to standard output. If you want to overwrite the | |
| 335 | + input file with the output, use the option | |
| 336 | + <option>--replace-input</option> and omit the output file name. | |
| 337 | + You can't specify the same file as both the input and the output. | |
| 338 | + If you do this, qpdf will tell you about the | |
| 339 | + <option>--replace-input</option> option. | |
| 338 | 340 | </para> |
| 339 | 341 | <para> |
| 340 | 342 | Most options require an output file, but some testing or |
| ... | ... | @@ -450,6 +452,21 @@ make |
| 450 | 452 | </listitem> |
| 451 | 453 | </varlistentry> |
| 452 | 454 | <varlistentry> |
| 455 | + <term><option>--replace-input</option></term> | |
| 456 | + <listitem> | |
| 457 | + <para> | |
| 458 | + If specified, the output file name should be omitted. This | |
| 459 | + option tells qpdf to replace the input file with the output. | |
| 460 | + It does this by writing to | |
| 461 | + <filename>.~qpdf-temp.<replaceable>infilename</replaceable>#</filename> | |
| 462 | + and, when done, overwriting the input file with the temporary | |
| 463 | + file. If there were any warnings, the original input is saved | |
| 464 | + as | |
| 465 | + <filename><replaceable>infilename</replaceable>.~qpdf-orig</filename>. | |
| 466 | + </para> | |
| 467 | + </listitem> | |
| 468 | + </varlistentry> | |
| 469 | + <varlistentry> | |
| 453 | 470 | <term><option>--copy-encryption=file</option></term> |
| 454 | 471 | <listitem> |
| 455 | 472 | <para> |
| ... | ... | @@ -4421,6 +4438,15 @@ print "\n"; |
| 4421 | 4438 | <itemizedlist> |
| 4422 | 4439 | <listitem> |
| 4423 | 4440 | <para> |
| 4441 | + The <option>--replace-input</option> option may be given in | |
| 4442 | + place of an output file name. This causes qpdf to overwrite | |
| 4443 | + the input file with the output. See the description of | |
| 4444 | + <option>--replace-input</option> in <xref | |
| 4445 | + linkend="ref.basic-options"/> for more details. | |
| 4446 | + </para> | |
| 4447 | + </listitem> | |
| 4448 | + <listitem> | |
| 4449 | + <para> | |
| 4424 | 4450 | The <option>--recompress-flate</option> instructs |
| 4425 | 4451 | <command>qpdf</command> to recompress streams that are |
| 4426 | 4452 | already compressed with <literal>/FlateDecode</literal>. | ... | ... |
qpdf/qpdf.cc
| ... | ... | @@ -23,6 +23,7 @@ |
| 23 | 23 | #include <qpdf/QPDFOutlineDocumentHelper.hh> |
| 24 | 24 | #include <qpdf/QPDFAcroFormDocumentHelper.hh> |
| 25 | 25 | #include <qpdf/QPDFExc.hh> |
| 26 | +#include <qpdf/QPDFSystemError.hh> | |
| 26 | 27 | |
| 27 | 28 | #include <qpdf/QPDFWriter.hh> |
| 28 | 29 | #include <qpdf/QIntC.hh> |
| ... | ... | @@ -180,6 +181,7 @@ struct Options |
| 180 | 181 | overlay("overlay"), |
| 181 | 182 | under_overlay(0), |
| 182 | 183 | require_outfile(true), |
| 184 | + replace_input(false), | |
| 183 | 185 | infilename(0), |
| 184 | 186 | outfilename(0) |
| 185 | 187 | { |
| ... | ... | @@ -283,6 +285,7 @@ struct Options |
| 283 | 285 | std::vector<PageSpec> page_specs; |
| 284 | 286 | std::map<std::string, RotationSpec> rotations; |
| 285 | 287 | bool require_outfile; |
| 288 | + bool replace_input; | |
| 286 | 289 | char const* infilename; |
| 287 | 290 | char const* outfilename; |
| 288 | 291 | }; |
| ... | ... | @@ -712,6 +715,7 @@ class ArgParser |
| 712 | 715 | void argUOrepeat(char* parameter); |
| 713 | 716 | void argUOpassword(char* parameter); |
| 714 | 717 | void argEndUnderOverlay(); |
| 718 | + void argReplaceInput(); | |
| 715 | 719 | |
| 716 | 720 | void usage(std::string const& message); |
| 717 | 721 | void checkCompletion(); |
| ... | ... | @@ -940,6 +944,7 @@ ArgParser::initOptionTable() |
| 940 | 944 | &ArgParser::argIiMinBytes, "minimum-bytes"); |
| 941 | 945 | (*t)["overlay"] = oe_bare(&ArgParser::argOverlay); |
| 942 | 946 | (*t)["underlay"] = oe_bare(&ArgParser::argUnderlay); |
| 947 | + (*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput); | |
| 943 | 948 | |
| 944 | 949 | t = &this->encrypt40_option_table; |
| 945 | 950 | (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); |
| ... | ... | @@ -1080,6 +1085,9 @@ ArgParser::argHelp() |
| 1080 | 1085 | << "will be interpreted as an argument. No interpolation is done. Line\n" |
| 1081 | 1086 | << "terminators are stripped. @- can be specified to read from standard input.\n" |
| 1082 | 1087 | << "\n" |
| 1088 | + << "The output file can be - to indicate writing to standard output, or it can\n" | |
| 1089 | + << "be --replace-input to cause qpdf to replace the input file with the output.\n" | |
| 1090 | + << "\n" | |
| 1083 | 1091 | << "Note that when contradictory options are provided, whichever options are\n" |
| 1084 | 1092 | << "provided last take precedence.\n" |
| 1085 | 1093 | << "\n" |
| ... | ... | @@ -1097,6 +1105,8 @@ ArgParser::argHelp() |
| 1097 | 1105 | << "--progress give progress indicators while writing output\n" |
| 1098 | 1106 | << "--no-warn suppress warnings\n" |
| 1099 | 1107 | << "--linearize generated a linearized (web optimized) file\n" |
| 1108 | + << "--replace-input use in place of specifying an output file; qpdf will\n" | |
| 1109 | + << " replace the input file with the output\n" | |
| 1100 | 1110 | << "--copy-encryption=file copy encryption parameters from specified file\n" |
| 1101 | 1111 | << "--encryption-file-password=password\n" |
| 1102 | 1112 | << " password used to open the file from which encryption\n" |
| ... | ... | @@ -2317,6 +2327,12 @@ ArgParser::argEndUnderOverlay() |
| 2317 | 2327 | } |
| 2318 | 2328 | |
| 2319 | 2329 | void |
| 2330 | +ArgParser::argReplaceInput() | |
| 2331 | +{ | |
| 2332 | + o.replace_input = true; | |
| 2333 | +} | |
| 2334 | + | |
| 2335 | +void | |
| 2320 | 2336 | ArgParser::handleArgFileArguments() |
| 2321 | 2337 | { |
| 2322 | 2338 | // Support reading arguments from files. Create a new argv. Ensure |
| ... | ... | @@ -3048,15 +3064,28 @@ ArgParser::doFinalChecks() |
| 3048 | 3064 | { |
| 3049 | 3065 | usage("missing -- at end of options"); |
| 3050 | 3066 | } |
| 3067 | + if (o.replace_input) | |
| 3068 | + { | |
| 3069 | + if (o.outfilename) | |
| 3070 | + { | |
| 3071 | + usage("--replace-input may not be used when" | |
| 3072 | + " an output file is specified"); | |
| 3073 | + } | |
| 3074 | + else if (o.split_pages) | |
| 3075 | + { | |
| 3076 | + usage("--split-pages may not be used with --replace-input"); | |
| 3077 | + } | |
| 3078 | + } | |
| 3051 | 3079 | if (o.infilename == 0) |
| 3052 | 3080 | { |
| 3053 | 3081 | usage("an input file name is required"); |
| 3054 | 3082 | } |
| 3055 | - else if (o.require_outfile && (o.outfilename == 0)) | |
| 3083 | + else if (o.require_outfile && (o.outfilename == 0) && (! o.replace_input)) | |
| 3056 | 3084 | { |
| 3057 | 3085 | usage("an output file name is required; use - for standard output"); |
| 3058 | 3086 | } |
| 3059 | - else if ((! o.require_outfile) && (o.outfilename != 0)) | |
| 3087 | + else if ((! o.require_outfile) && | |
| 3088 | + ((o.outfilename != 0) || o.replace_input)) | |
| 3060 | 3089 | { |
| 3061 | 3090 | usage("no output file may be given for this option"); |
| 3062 | 3091 | } |
| ... | ... | @@ -3065,7 +3094,8 @@ ArgParser::doFinalChecks() |
| 3065 | 3094 | o.externalize_inline_images = true; |
| 3066 | 3095 | } |
| 3067 | 3096 | |
| 3068 | - if (o.require_outfile && (strcmp(o.outfilename, "-") == 0)) | |
| 3097 | + if (o.require_outfile && o.outfilename && | |
| 3098 | + (strcmp(o.outfilename, "-") == 0)) | |
| 3069 | 3099 | { |
| 3070 | 3100 | if (o.split_pages) |
| 3071 | 3101 | { |
| ... | ... | @@ -3088,7 +3118,7 @@ ArgParser::doFinalChecks() |
| 3088 | 3118 | { |
| 3089 | 3119 | QTC::TC("qpdf", "qpdf same file error"); |
| 3090 | 3120 | usage("input file and output file are the same;" |
| 3091 | - " this would cause input file to be lost"); | |
| 3121 | + " use --replace-input to intentionally overwrite the input file"); | |
| 3092 | 3122 | } |
| 3093 | 3123 | } |
| 3094 | 3124 | |
| ... | ... | @@ -3861,6 +3891,12 @@ static void do_inspection(QPDF& pdf, Options& o) |
| 3861 | 3891 | { |
| 3862 | 3892 | do_show_pages(pdf, o); |
| 3863 | 3893 | } |
| 3894 | + if ((! pdf.getWarnings().empty()) && (exit_code != EXIT_ERROR)) | |
| 3895 | + { | |
| 3896 | + std::cerr << whoami | |
| 3897 | + << ": operation succeeded with warnings" << std::endl; | |
| 3898 | + exit_code = EXIT_WARNING; | |
| 3899 | + } | |
| 3864 | 3900 | if (exit_code) |
| 3865 | 3901 | { |
| 3866 | 3902 | exit(exit_code); |
| ... | ... | @@ -5109,18 +5145,80 @@ static void do_split_pages(QPDF& pdf, Options& o) |
| 5109 | 5145 | |
| 5110 | 5146 | static void write_outfile(QPDF& pdf, Options& o) |
| 5111 | 5147 | { |
| 5112 | - if (strcmp(o.outfilename, "-") == 0) | |
| 5148 | + std::string temp_out; | |
| 5149 | + if (o.replace_input) | |
| 5150 | + { | |
| 5151 | + // Use a file name that is hidden by default in the OS to | |
| 5152 | + // avoid having it become momentarily visible in a | |
| 5153 | + // graphical file manager or in case it gets left behind | |
| 5154 | + // because of some kind of error. | |
| 5155 | + temp_out = ".~qpdf-temp." + std::string(o.infilename) + "#"; | |
| 5156 | + // o.outfilename will be restored to 0 before temp_out | |
| 5157 | + // goes out of scope. | |
| 5158 | + o.outfilename = temp_out.c_str(); | |
| 5159 | + } | |
| 5160 | + else if (strcmp(o.outfilename, "-") == 0) | |
| 5113 | 5161 | { |
| 5114 | 5162 | o.outfilename = 0; |
| 5115 | 5163 | } |
| 5116 | - QPDFWriter w(pdf, o.outfilename); | |
| 5117 | - set_writer_options(pdf, o, w); | |
| 5118 | - w.write(); | |
| 5119 | - if (o.verbose) | |
| 5164 | + { | |
| 5165 | + // Private scope so QPDFWriter will close the output file | |
| 5166 | + QPDFWriter w(pdf, o.outfilename); | |
| 5167 | + set_writer_options(pdf, o, w); | |
| 5168 | + w.write(); | |
| 5169 | + } | |
| 5170 | + if (o.verbose && o.outfilename) | |
| 5120 | 5171 | { |
| 5121 | 5172 | std::cout << whoami << ": wrote file " |
| 5122 | 5173 | << o.outfilename << std::endl; |
| 5123 | 5174 | } |
| 5175 | + if (o.replace_input) | |
| 5176 | + { | |
| 5177 | + o.outfilename = 0; | |
| 5178 | + } | |
| 5179 | + if (o.replace_input) | |
| 5180 | + { | |
| 5181 | + // We must close the input before we can rename files | |
| 5182 | + pdf.closeInputSource(); | |
| 5183 | + std::string backup; | |
| 5184 | + bool warnings = pdf.anyWarnings(); | |
| 5185 | + if (warnings) | |
| 5186 | + { | |
| 5187 | + // If there are warnings, the user may care about this | |
| 5188 | + // file, so give it a non-hidden name that will be | |
| 5189 | + // lexically grouped with the original file. | |
| 5190 | + backup = std::string(o.infilename) + ".~qpdf-orig"; | |
| 5191 | + } | |
| 5192 | + else | |
| 5193 | + { | |
| 5194 | + backup = ".~qpdf-orig." + std::string(o.infilename) + "#"; | |
| 5195 | + } | |
| 5196 | + QUtil::rename_file(o.infilename, backup.c_str()); | |
| 5197 | + QUtil::rename_file(temp_out.c_str(), o.infilename); | |
| 5198 | + if (warnings) | |
| 5199 | + { | |
| 5200 | + std::cerr << whoami | |
| 5201 | + << ": there are warnings; original file kept in " | |
| 5202 | + << backup << std::endl; | |
| 5203 | + } | |
| 5204 | + else | |
| 5205 | + { | |
| 5206 | + try | |
| 5207 | + { | |
| 5208 | + QUtil::remove_file(backup.c_str()); | |
| 5209 | + } | |
| 5210 | + catch (QPDFSystemError& e) | |
| 5211 | + { | |
| 5212 | + std::cerr | |
| 5213 | + << whoami | |
| 5214 | + << ": unable to delete original file (" | |
| 5215 | + << e.what() << ");" | |
| 5216 | + << " original file left in " << backup | |
| 5217 | + << ", but the input was successfully replaced" | |
| 5218 | + << std::endl; | |
| 5219 | + } | |
| 5220 | + } | |
| 5221 | + } | |
| 5124 | 5222 | } |
| 5125 | 5223 | |
| 5126 | 5224 | int realmain(int argc, char* argv[]) |
| ... | ... | @@ -5156,7 +5254,7 @@ int realmain(int argc, char* argv[]) |
| 5156 | 5254 | handle_under_overlay(pdf, o); |
| 5157 | 5255 | handle_transformations(pdf, o); |
| 5158 | 5256 | |
| 5159 | - if (o.outfilename == 0) | |
| 5257 | + if ((o.outfilename == 0) && (! o.replace_input)) | |
| 5160 | 5258 | { |
| 5161 | 5259 | do_inspection(pdf, o); |
| 5162 | 5260 | } | ... | ... |
qpdf/qtest/qpdf.test
| ... | ... | @@ -191,6 +191,47 @@ foreach my $d (['auto-ü', 1], ['auto-öπ', 2]) |
| 191 | 191 | |
| 192 | 192 | show_ntests(); |
| 193 | 193 | # ---------- |
| 194 | +$td->notify("--- Replace Input ---"); | |
| 195 | +$n_tests += 8; | |
| 196 | + | |
| 197 | +# Use Unicode file names to test replace input so we can be sure it | |
| 198 | +# works for that case. | |
| 199 | +$td->runtest("create unicode filenames", | |
| 200 | + {$td->COMMAND => "test_unicode_filenames"}, | |
| 201 | + {$td->STRING => "created Unicode filenames\n", | |
| 202 | + $td->EXIT_STATUS => 0}, | |
| 203 | + $td->NORMALIZE_NEWLINES); | |
| 204 | + | |
| 205 | +foreach my $d (['auto-ü', 1], ['auto-öπ', 2]) | |
| 206 | +{ | |
| 207 | + my ($u, $n) = @$d; | |
| 208 | + $td->runtest("replace input $u", | |
| 209 | + {$td->COMMAND => "qpdf --deterministic-id" . | |
| 210 | + " --object-streams=generate --replace-input $u.pdf"}, | |
| 211 | + {$td->STRING => "", $td->EXIT_STATUS => 0}, | |
| 212 | + $td->NORMALIZE_NEWLINES); | |
| 213 | + $td->runtest("check output ($u)", | |
| 214 | + {$td->FILE => "$u.pdf"}, | |
| 215 | + {$td->FILE => "replace-input.pdf"}, | |
| 216 | + $td->NORMALIZE_NEWLINES); | |
| 217 | +} | |
| 218 | + | |
| 219 | +system("cp xref-with-short-size.pdf auto-warn.pdf") == 0 or die; | |
| 220 | +$td->runtest("replace input with warnings", | |
| 221 | + {$td->COMMAND => | |
| 222 | + "qpdf --deterministic-id --replace-input auto-warn.pdf"}, | |
| 223 | + {$td->FILE => "replace-warn.out", $td->EXIT_STATUS => 3}, | |
| 224 | + $td->NORMALIZE_NEWLINES); | |
| 225 | + | |
| 226 | +$td->runtest("check output", | |
| 227 | + {$td->FILE => "auto-warn.pdf"}, | |
| 228 | + {$td->FILE => "warn-replace.pdf"}); | |
| 229 | +$td->runtest("check orig output", | |
| 230 | + {$td->FILE => "auto-warn.pdf.~qpdf-orig"}, | |
| 231 | + {$td->FILE => "xref-with-short-size.pdf"}); | |
| 232 | + | |
| 233 | +show_ntests(); | |
| 234 | +# ---------- | |
| 194 | 235 | $td->notify("--- Final Version ---"); |
| 195 | 236 | $n_tests += 1; |
| 196 | 237 | |
| ... | ... | @@ -4233,5 +4274,5 @@ sub get_md5_checksum |
| 4233 | 4274 | sub cleanup |
| 4234 | 4275 | { |
| 4235 | 4276 | system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache"); |
| 4236 | - system("rm -rf *split-out* ???-kfo.pdf *.tmpout \@file.pdf auto-*.pdf"); | |
| 4277 | + system("rm -rf *split-out* ???-kfo.pdf *.tmpout \@file.pdf auto-*"); | |
| 4237 | 4278 | } | ... | ... |
qpdf/qtest/qpdf/bad-jpeg-show.out
qpdf/qtest/qpdf/empty-object.out
qpdf/qtest/qpdf/replace-input.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/replace-warn.out
0 → 100644
qpdf/qtest/qpdf/warn-replace.pdf
0 → 100644
No preview for this file type
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 |
| 11 | 11 | 10/0: compressed; stream = 5, index = 3 |
| 12 | 12 | 11/0: compressed; stream = 5, index = 7 |
| 13 | 13 | 12/0: compressed; stream = 5, index = 8 |
| 14 | -qpdf: operation succeeded with warnings; resulting file may have some problems | |
| 14 | +qpdf: operation succeeded with warnings | ... | ... |