diff --git a/examples/qpdf-job.cc b/examples/qpdf-job.cc index 3e6e99bd..c2952ee5 100644 --- a/examples/qpdf-job.cc +++ b/examples/qpdf-job.cc @@ -40,7 +40,9 @@ main(int argc, char* argv[]) ->inputFile("in.pdf") ->outputFile("out1.pdf") ->pages() - ->pageSpec(".", "1") + // Prior to qpdf 11.9.0, call ->pageSpec(file, range, password) + ->file(".") + ->range("1") ->endPages() ->linearize() ->staticId() // for testing only diff --git a/include/qpdf/QPDFJob.hh b/include/qpdf/QPDFJob.hh index 4626c264..443fa75c 100644 --- a/include/qpdf/QPDFJob.hh +++ b/include/qpdf/QPDFJob.hh @@ -243,6 +243,8 @@ class QPDFJob public: QPDF_DLL Config* endPages(); + // From qpdf 11.9.0, you can call file(), range(), and password(). Each call to file() + // starts a new page spec. QPDF_DLL PagesConfig* pageSpec( std::string const& filename, std::string const& range, char const* password = nullptr); diff --git a/include/qpdf/auto_job_c_pages.hh b/include/qpdf/auto_job_c_pages.hh index b816d29d..75b0ae53 100644 --- a/include/qpdf/auto_job_c_pages.hh +++ b/include/qpdf/auto_job_c_pages.hh @@ -5,3 +5,6 @@ // // clang-format off // +QPDF_DLL PagesConfig* file(std::string const& parameter); +QPDF_DLL PagesConfig* range(std::string const& parameter); +QPDF_DLL PagesConfig* password(std::string const& parameter); diff --git a/job.sums b/job.sums index 713d4379..26bbcac8 100644 --- a/job.sums +++ b/job.sums @@ -5,16 +5,16 @@ include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf2 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 include/qpdf/auto_job_c_main.hh dbfc221d1533120d1aa9c361d8d2483dea5fcb1c0fd95144d98d305e64ed32a6 -include/qpdf/auto_job_c_pages.hh b3cc0f21029f6d89efa043dcdbfa183cb59325b6506001c18911614fe8e568ec +include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 include/qpdf/auto_job_c_uo.hh ae21b69a1efa9333050f4833d465f6daff87e5b38e5106e49bbef5d4132e4ed1 -job.yml 8ad309ac41520b34692bcf22fd5c2ef4810ff69562aed606bd57df7bf589bc43 -libqpdf/qpdf/auto_job_decl.hh 1e8d73891bd1f0b5df5a5ca7405fb76d2d0fd024941b8c1b86489f1b5f9c5772 -libqpdf/qpdf/auto_job_help.hh 8c172913920a5273e04dc4d2059f2d78fc475960ac1738271357056beb02dd27 -libqpdf/qpdf/auto_job_init.hh ea272fd6a6a5e4d23cabd70a7b7d5ecc543b6304008c656dcba2d353d378efc2 -libqpdf/qpdf/auto_job_json_decl.hh 10ffb0d0e5ca09809a5d5d78f66dee393dfd2653a23441436465fd5ace151880 -libqpdf/qpdf/auto_job_json_init.hh 9c3839877ab3b15a47e92086f0b5616da33fd4970538cc423d3b0a7ff33ce66a -libqpdf/qpdf/auto_job_schema.hh a882939b202d48ad1c0751c094f671ad7aad0fc04c3a4446ad83675db365c8a2 +job.yml 45761edeca048c7aa3e99340fcda1b6cd8efe4cc4c8b8a6628580243a4f49b57 +libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6 +libqpdf/qpdf/auto_job_help.hh b19f8a7433c70df70b42f893a8964c801aa2bb78eecaa13cffab7add2ff81e0a +libqpdf/qpdf/auto_job_init.hh d74759d4999201a89dafddf6f0c855e9151bbf77ea91a92d6806510292950123 +libqpdf/qpdf/auto_job_json_decl.hh 485540cde820987cfbed0aa7642a6416f2bd37164c8d4f2322f1381e73edf903 +libqpdf/qpdf/auto_job_json_init.hh c8de8658daa82115b49bf084cebe1be0b8aea73f864a219d7349acc0982b56fe +libqpdf/qpdf/auto_job_schema.hh 3e000b87255bee62ba29b794d67b2ae97cbbdfdb78be3878c51786913564901e manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst 43923fab0def5f76e537f03ba2f650270a1ae858a15747e355db2a6ea396f1a0 -manual/qpdf.1 cd335812d450ca83be3c7fe165299d3454b26b4999295f671d57e6b24f6ea7a1 +manual/cli.rst 408e17dc13d37befe34badc400dd34d3c283952d17ee3bf9a9d44898af3dabc7 +manual/qpdf.1 c99d66833aee7a2294176875ca2e9ddf2531d4ab8fb282ea5c45cb82a5d028ea manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b diff --git a/job.yml b/job.yml index 3d918385..d82111ad 100644 --- a/job.yml +++ b/job.yml @@ -195,9 +195,9 @@ options: config: c_pages prefix: Pages positional: true - manual: - - password required_parameter: + file: file + range: page-range password: password - table: encryption config: c_main @@ -436,9 +436,9 @@ json: oi-min-width: optimize-images: pages: - - _file: "source for for pages" + - file: Pages.password: - _range: "page range" + range: remove-page-labels: report-memory-usage: rotate: diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index 3bdb4f39..d82d6fc9 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -2342,6 +2342,9 @@ QPDFJob::handlePageSpecs(QPDF& pdf, std::vector>& page_hea if (page_spec.filename == ".") { page_spec.filename = m->infilename.get(); } + if (page_spec.range.empty()) { + page_spec.range = "1-z"; + } } if (!m->keep_files_open_set) { diff --git a/libqpdf/QPDFJob_argv.cc b/libqpdf/QPDFJob_argv.cc index 737059f0..ea35342b 100644 --- a/libqpdf/QPDFJob_argv.cc +++ b/libqpdf/QPDFJob_argv.cc @@ -34,9 +34,10 @@ namespace std::shared_ptr c_uo; std::shared_ptr c_enc; std::vector accumulated_args; - std::shared_ptr pages_password{nullptr}; std::string user_password; std::string owner_password; + bool called_pages_file{false}; + bool called_pages_range{false}; bool used_enc_password_args{false}; bool gave_input{false}; bool gave_output{false}; @@ -236,82 +237,44 @@ ArgParser::argPages() this->ap.selectOptionTable(O_PAGES); } -void -ArgParser::argPagesPassword(std::string const& parameter) -{ - if (this->pages_password) { - QTC::TC("qpdf", "QPDFJob duplicated pages password"); - usage("--password already specified for this file"); - } - if (this->accumulated_args.size() != 1) { - QTC::TC("qpdf", "QPDFJob misplaced pages password"); - usage("in --pages, --password must immediately follow a file name"); - } - this->pages_password = QUtil::make_shared_cstr(parameter); -} - void ArgParser::argPagesPositional(std::string const& arg) { - if (arg.empty()) { - if (this->accumulated_args.empty()) { - return; - } - } else { - this->accumulated_args.push_back(arg); + if (!called_pages_file) { + c_pages->file(arg); + called_pages_file = true; + return; } - - std::string file = this->accumulated_args.at(0); - char const* range_p = nullptr; - - size_t n_args = this->accumulated_args.size(); - if (n_args >= 2) { - // will be copied before accumulated_args is cleared - range_p = this->accumulated_args.at(1).c_str(); + if (called_pages_range) { + c_pages->file(arg); + called_pages_range = false; + return; } - - // See if the user omitted the range entirely, in which case we assume "1-z". - std::string next_file; - if (range_p == nullptr) { - if (arg.empty()) { - // The filename or password was the last argument - QTC::TC("qpdf", "QPDFJob pages range omitted at end", this->pages_password ? 0 : 1); + // This could be a range or a file. Try parsing. + try { + QUtil::parse_numrange(arg.c_str(), 0); + c_pages->range(arg); + called_pages_range = true; + } catch (std::runtime_error& e1) { + // The range is invalid. Let's see if it's a file. + if (arg == ".") { + // "." means the input file. + QTC::TC("qpdf", "QPDFJob pages range omitted with ."); + } else if (QUtil::file_can_be_opened(arg.c_str())) { + QTC::TC("qpdf", "QPDFJob pages range omitted in middle"); + // Yup, it's a file. } else { - // We need to accumulate some more arguments - return; + // Give the range error + usage(e1.what()); } - } else { - try { - QUtil::parse_numrange(range_p, 0); - } catch (std::runtime_error& e1) { - // The range is invalid. Let's see if it's a file. - if (strcmp(range_p, ".") == 0) { - // "." means the input file. - QTC::TC("qpdf", "QPDFJob pages range omitted with ."); - } else if (QUtil::file_can_be_opened(range_p)) { - QTC::TC("qpdf", "QPDFJob pages range omitted in middle"); - // Yup, it's a file. - } else { - // Give the range error - usage(e1.what()); - } - next_file = range_p; - range_p = nullptr; - } - } - std::string range(range_p ? range_p : "1-z"); - this->c_pages->pageSpec(file, range, this->pages_password.get()); - this->accumulated_args.clear(); - this->pages_password = nullptr; - if (!next_file.empty()) { - this->accumulated_args.push_back(next_file); + c_pages->file(arg); + called_pages_range = false; } } void ArgParser::argEndPages() { - argPagesPositional(""); c_pages->endPages(); c_pages = nullptr; } diff --git a/libqpdf/QPDFJob_config.cc b/libqpdf/QPDFJob_config.cc index db6c5948..c43cc82f 100644 --- a/libqpdf/QPDFJob_config.cc +++ b/libqpdf/QPDFJob_config.cc @@ -968,6 +968,45 @@ QPDFJob::PagesConfig::pageSpec( return this; } +QPDFJob::PagesConfig* +QPDFJob::PagesConfig::file(std::string const& arg) +{ + this->config->o.m->page_specs.emplace_back(arg, nullptr, ""); + return this; +} + +QPDFJob::PagesConfig* +QPDFJob::PagesConfig::range(std::string const& arg) +{ + if (config->o.m->page_specs.empty()) { + QTC::TC("qpdf", "QPDFJob misplaced page range"); + usage("in --range must follow a file name"); + } + auto& last = config->o.m->page_specs.back(); + if (!last.range.empty()) { + QTC::TC("qpdf", "QPDFJob duplicated range"); + usage("--range already specified for this file"); + } + last.range = arg; + return this; +} + +QPDFJob::PagesConfig* +QPDFJob::PagesConfig::password(std::string const& arg) +{ + if (config->o.m->page_specs.empty()) { + QTC::TC("qpdf", "QPDFJob misplaced pages password"); + usage("in --pages, --password must follow a file name"); + } + auto& last = config->o.m->page_specs.back(); + if (last.password) { + QTC::TC("qpdf", "QPDFJob duplicated pages password"); + usage("--password already specified for this file"); + } + last.password = QUtil::make_shared_cstr(arg); + return this; +} + std::shared_ptr QPDFJob::Config::overlay() { diff --git a/libqpdf/QPDFJob_json.cc b/libqpdf/QPDFJob_json.cc index 754cb81b..5565ea93 100644 --- a/libqpdf/QPDFJob_json.cc +++ b/libqpdf/QPDFJob_json.cc @@ -467,25 +467,17 @@ Handlers::endPagesArray() void Handlers::beginPages(JSON j) { - std::string file; - std::string range("1-z"); - std::string password; bool file_seen = false; - bool password_seen = false; - j.forEachDictItem([&](std::string const& key, JSON value) { + j.forEachDictItem([&](std::string const& key, JSON const& value) { if (key == "file") { - file_seen = value.getString(file); - } else if (key == "range") { - value.getString(range); - } else if (key == "password") { - password_seen = value.getString(password); + std::string v; + file_seen = value.getString(v); } }); if (!file_seen) { QTC::TC("qpdf", "QPDFJob json pages no file"); usage("file is required in page specification"); } - this->c_pages->pageSpec(file, range, password_seen ? password.c_str() : nullptr); } void @@ -494,25 +486,10 @@ Handlers::endPages() // nothing needed } -void -Handlers::setupPagesFile() -{ - // handled in beginPages - ignoreItem(); -} - void Handlers::setupPagesPassword() { - // handled in beginPages - ignoreItem(); -} - -void -Handlers::setupPagesRange() -{ - // handled in beginPages - ignoreItem(); + addParameter([this](char const* p) { c_pages->password(p); }); } void diff --git a/libqpdf/qpdf/auto_job_decl.hh b/libqpdf/qpdf/auto_job_decl.hh index 38f7cb32..d70af259 100644 --- a/libqpdf/qpdf/auto_job_decl.hh +++ b/libqpdf/qpdf/auto_job_decl.hh @@ -31,7 +31,6 @@ void argReplaceInput(); void argSetPageLabels(); void argUnderlay(); void argPagesPositional(std::string const&); -void argPagesPassword(std::string const&); void argEndPages(); void argEncPositional(std::string const&); void argEncUserPassword(std::string const&); diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index 35c1a0d2..a3572aed 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/libqpdf/qpdf/auto_job_help.hh @@ -311,10 +311,24 @@ static void add_help_4(QPDFArgParser& ap) ap.addHelpTopic("modification", "change parts of the PDF", R"(Modification options make systematic changes to certain parts of the PDF, causing the PDF to render differently from the original. )"); -ap.addOptionHelp("--pages", "modification", "begin page selection", R"(--pages file [--password=password] [page-range] [...] -- +ap.addOptionHelp("--pages", "modification", "begin page selection", R"(--pages [--file=]file [options] [...] -- Run qpdf --help=page-selection for details. )"); +ap.addOptionHelp("--file", "modification", "source for pages", R"(--file=file + +Specify the file for the current page operation. This is used +with --pages, --overlay, and --underlay and appears between the +option and the terminating --. Run qpdf --help=page-selection +for details. +)"); +ap.addOptionHelp("--range", "modification", "page range", R"(--range=numeric-range + +Specify the page range for the current page operation with +--pages. If omitted, all pages are selected. This is used +with --pages and appears between --pages and --. Run +qpdf --help=page-selection for details. +)"); ap.addOptionHelp("--collate", "modification", "collate with --pages", R"(--collate[=n[,m,...]] Collate rather than concatenate pages specified with --pages. @@ -437,6 +451,9 @@ iv, then the remaining pages with Arabic numerals starting with 1 and continuing sequentially until the end of the document. For additional examples, please consult the manual. )"); +} +static void add_help_5(QPDFArgParser& ap) +{ ap.addHelpTopic("encryption", "create encrypted files", R"(Create encrypted files. Usage: --encrypt \ @@ -520,9 +537,6 @@ ap.addOptionHelp("--user-password", "encryption", "specify user password", R"(-- Set the user password of the encrypted file. )"); -} -static void add_help_5(QPDFArgParser& ap) -{ ap.addOptionHelp("--owner-password", "encryption", "specify owner password", R"(--owner-password=owner-password Set the owner password of the encrypted file. @@ -620,12 +634,24 @@ should not be used except for compatibility testing. )"); ap.addHelpTopic("page-selection", "select pages from one or more files", R"(Use the --pages option to select pages from multiple files. Usage: +qpdf in.pdf --pages --file=input-file \ + [--range=page-range] [--password=password] [...] -- out.pdf + +OR + qpdf in.pdf --pages input-file [--password=password] [page-range] \ [...] -- out.pdf Between --pages and the -- that terminates pages option, repeat the following: +--file=filename [--range=page-range] [--password=password] [options] + +For compatibility, the file and range can be specified +positionally. qpdf versions prior to 11.9.0 +require --password=password to immediately follow the filename. In +the older syntax, repeat the following: + filename [--password=password] [page-range] Document-level information, such as outlines, tags, etc., is taken @@ -654,7 +680,7 @@ Examples: information from in.pdf is retained. Note the use of "." to refer to in.pdf. - qpdf in.pdf --pages . a.pdf b.pdf:even -- out.pdf + qpdf in.pdf --pages . a.pdf b.pdf 1-z:even -- out.pdf - Take all the pages from a.pdf, all the pages from b.pdf in reverse, and only pages 3 and 6 from c.pdf and write the result @@ -687,6 +713,9 @@ those pages to be repeated after the original pages are exhausted. Run qpdf --help=page-ranges for help with page ranges. )"); +} +static void add_help_6(QPDFArgParser& ap) +{ ap.addOptionHelp("--to", "overlay-underlay", "destination pages for underlay/overlay", R"(--to=page-range Specify the range of pages in the primary output to apply @@ -700,9 +729,6 @@ the destination pages. See qpdf --help=page-ranges for help with the page range syntax. The page range may be omitted if --repeat is used. )"); -} -static void add_help_6(QPDFArgParser& ap) -{ ap.addOptionHelp("--repeat", "overlay-underlay", "overlay/underlay pages to repeat", R"(--repeat=page-range Specify pages from the overlay/underlay that are repeated after @@ -801,6 +827,9 @@ ap.addHelpTopic("inspection", "inspect PDF files", R"(These options provide tool the options in this section are specified, no output file may be given. )"); +} +static void add_help_7(QPDFArgParser& ap) +{ ap.addOptionHelp("--is-encrypted", "inspection", "silently test whether a file is encrypted", R"(Silently exit with a code indicating the file's encryption status: 0: the file is encrypted @@ -817,9 +846,6 @@ ap.addOptionHelp("--requires-password", "inspection", "silently test a file's pa 2: the file is not encrypted 3: the file is encrypted, and correct password (if any) has been supplied )"); -} -static void add_help_7(QPDFArgParser& ap) -{ ap.addOptionHelp("--check", "inspection", "partially check whether PDF is valid", R"(Check the structure of the PDF file as well as a number of other aspects of the file, and write information about the file to standard output. Note that qpdf does not perform any validation @@ -894,6 +920,9 @@ Describe the format of the JSON output by writing to standard output a JSON object with the same keys and with values containing descriptive text. )"); +} +static void add_help_8(QPDFArgParser& ap) +{ ap.addOptionHelp("--json-key", "json", "limit which keys are in JSON output", R"(--json-key=key This option is repeatable. If given, only the specified @@ -907,9 +936,6 @@ This option is repeatable. If given, only specified objects will be shown in the "objects" key of the JSON output. Otherwise, all objects will be shown. )"); -} -static void add_help_8(QPDFArgParser& ap) -{ ap.addOptionHelp("--json-stream-data", "json", "how to handle streams in json output", R"(--json-stream-data={none|inline|file} When used with --json, this option controls whether streams in diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index 6ac88762..401406db 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -127,7 +127,9 @@ this->ap.addChoices("json", [this](std::string const& x){c_main->json(x);}, fals this->ap.addChoices("json-output", [this](std::string const& x){c_main->jsonOutput(x);}, false, json_output_choices); this->ap.registerOptionTable("pages", b(&ArgParser::argEndPages)); this->ap.addPositional(p(&ArgParser::argPagesPositional)); -this->ap.addRequiredParameter("password", p(&ArgParser::argPagesPassword), "password"); +this->ap.addRequiredParameter("file", [this](std::string const& x){c_pages->file(x);}, "file"); +this->ap.addRequiredParameter("range", [this](std::string const& x){c_pages->range(x);}, "page-range"); +this->ap.addRequiredParameter("password", [this](std::string const& x){c_pages->password(x);}, "password"); this->ap.registerOptionTable("encryption", b(&ArgParser::argEndEncryption)); this->ap.addPositional(p(&ArgParser::argEncPositional)); this->ap.addRequiredParameter("user-password", p(&ArgParser::argEncUserPassword), "user_password"); diff --git a/libqpdf/qpdf/auto_job_json_decl.hh b/libqpdf/qpdf/auto_job_json_decl.hh index f3576494..b2ae4c1e 100644 --- a/libqpdf/qpdf/auto_job_json_decl.hh +++ b/libqpdf/qpdf/auto_job_json_decl.hh @@ -41,9 +41,7 @@ void beginPagesArray(JSON); void endPagesArray(); void beginPages(JSON); void endPages(); -void setupPagesFile(); void setupPagesPassword(); -void setupPagesRange(); void beginSetPageLabelsArray(JSON); void endSetPageLabelsArray(); void setupSetPageLabels(); diff --git a/libqpdf/qpdf/auto_job_json_init.hh b/libqpdf/qpdf/auto_job_json_init.hh index 22621271..fb4d93b9 100644 --- a/libqpdf/qpdf/auto_job_json_init.hh +++ b/libqpdf/qpdf/auto_job_json_init.hh @@ -402,13 +402,13 @@ pushKey("pages"); beginArray(bindJSON(&Handlers::beginPagesArray), bindBare(&Handlers::endPagesArray)); // .pages[] beginDict(bindJSON(&Handlers::beginPages), bindBare(&Handlers::endPages)); // .pages pushKey("file"); -setupPagesFile(); +addParameter([this](std::string const& p) { c_pages->file(p); }); popHandler(); // key: file pushKey("password"); setupPagesPassword(); popHandler(); // key: password pushKey("range"); -setupPagesRange(); +addParameter([this](std::string const& p) { c_pages->range(p); }); popHandler(); // key: range popHandler(); // array: .pages[] popHandler(); // key: pages diff --git a/libqpdf/qpdf/auto_job_schema.hh b/libqpdf/qpdf/auto_job_schema.hh index f7b8590c..bb4b04ea 100644 --- a/libqpdf/qpdf/auto_job_schema.hh +++ b/libqpdf/qpdf/auto_job_schema.hh @@ -140,7 +140,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ "optimizeImages": "use efficient compression for images", "pages": [ { - "file": "source for for pages", + "file": "source for pages", "password": "password for encrypted file", "range": "page range" } diff --git a/manual/cli.rst b/manual/cli.rst index f02fd17d..70253a5f 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -1388,7 +1388,7 @@ PDF, causing the PDF to render differently from the original. See also Related Options ~~~~~~~~~~~~~~~ -.. qpdf:option:: --pages file [--password=password] [page-range] [...] -- +.. qpdf:option:: --pages [--file=]file [options] [...] -- .. help: begin page selection @@ -1403,6 +1403,38 @@ Related Options See also :qpdf:ref:`--split-pages`, :qpdf:ref:`--collate`, :ref:`page-ranges`. +.. qpdf:option:: --file=file + + .. help: source for pages + + Specify the file for the current page operation. This is used + with --pages, --overlay, and --underlay and appears between the + option and the terminating --. Run qpdf --help=page-selection + for details. + + Specify the file for the current page operation. This option is + used with :qpdf:ref:`--pages`, :qpdf:ref:`--overlay` and + :qpdf:ref:`--underlay` and appears between the option and the + terminating ``--``. + + Please see :ref:`page-selection` for additional details. + +.. qpdf:option:: --range=numeric-range + + .. help: page range + + Specify the page range for the current page operation with + --pages. If omitted, all pages are selected. This is used + with --pages and appears between --pages and --. Run + qpdf --help=page-selection for details. + + Specify the page range for the current page operation with + :qpdf:ref:`--pages`. If omitted, all pages are selected. This + option is used with :qpdf:ref:`--pages` and appears between + :qpdf:ref:`--pages` and ``--``. + + Please see :ref:`page-selection` for additional details. + .. qpdf:option:: --collate[=n[,m,...]] .. help: collate with --pages @@ -2424,12 +2456,24 @@ Page Selection Use the --pages option to select pages from multiple files. Usage: + qpdf in.pdf --pages --file=input-file \ + [--range=page-range] [--password=password] [...] -- out.pdf + + OR + qpdf in.pdf --pages input-file [--password=password] [page-range] \ [...] -- out.pdf Between --pages and the -- that terminates pages option, repeat the following: + --file=filename [--range=page-range] [--password=password] [options] + + For compatibility, the file and range can be specified + positionally. qpdf versions prior to 11.9.0 + require --password=password to immediately follow the filename. In + the older syntax, repeat the following: + filename [--password=password] [page-range] Document-level information, such as outlines, tags, etc., is taken @@ -2458,7 +2502,7 @@ Page Selection information from in.pdf is retained. Note the use of "." to refer to in.pdf. - qpdf in.pdf --pages . a.pdf b.pdf:even -- out.pdf + qpdf in.pdf --pages . a.pdf b.pdf 1-z:even -- out.pdf - Take all the pages from a.pdf, all the pages from b.pdf in reverse, and only pages 3 and 6 from c.pdf and write the result @@ -2472,14 +2516,29 @@ Page Selection split and merge PDF files by selecting pages from one or more input files. -Usage: :samp:`qpdf {in.pdf} --pages input-file [--password={password}] [{page-range}] [...] -- {out.pdf}` +:: -Between ``--pages`` and the ``--`` that terminates pages option, -repeat the following: + qpdf primary-input.pdf \ + --file=input.pdf \ + [--range=page-range] \ + [--password=password] \ + [...] \ + -- output.pdf -:samp:`{filename} [--password={password}] [{page-range}]` +OR + +:: + + qpdf primary-input.pdf \ + input.pdf [--password=password] [page-range] \ + [...] -- output.pdf Notes: + - The first form, with :qpdf:ref:`--file` and :qpdf:ref:`--range`, + was introduced in qpdf 11.9.0. In this form, the + :qpdf:ref:`--range` and :qpdf:ref:`--password` options apply to + the most recently specified :qpdf:ref:`--file` option. + - The password option is needed only for password-protected files. If you specify the same file more than once, you only need to supply the password the first time. @@ -2518,8 +2577,7 @@ Examples :: - qpdf in.pdf --pages . a.pdf b.pdf:even -- out.pdf - + qpdf in.pdf --pages . a.pdf b.pdf 1-z:even -- out.pdf - Take all the pages from :file:`a.pdf`, all the pages from :file:`b.pdf` in reverse, and only pages 3 and 6 from :file:`c.pdf` @@ -2529,7 +2587,9 @@ Examples :: - qpdf --empty --pages a.pdf b.pdf --password=x z-1 c.pdf 3,6 + qpdf --empty --pages --file=a.pdf \ + --file=b.pdf --password=x --range=z-1 \ + --file=c.pdf --range=3,6 -- out.pdf - Scan a document with double-sided printing by scanning the fronts into :file:`odd.pdf` and the backs into :file:`even.pdf`. Collate @@ -2542,6 +2602,8 @@ Examples qpdf --collate odd.pdf --pages . even.pdf -- all.pdf OR qpdf --collate --empty --pages odd.pdf even.pdf -- all.pdf + OR + qpdf --collate --empty --pages --file=odd.pdf --file=even.pdf -- all.pdf - When collating, any number of files and page ranges can be specified. If any file has fewer pages, that file is just skipped diff --git a/manual/qpdf.1 b/manual/qpdf.1 index d82fe416..4de6427d 100644 --- a/manual/qpdf.1 +++ b/manual/qpdf.1 @@ -409,10 +409,26 @@ the PDF, causing the PDF to render differently from the original. Related Options: .TP .B --pages \-\- begin page selection ---pages file [--password=password] [page-range] [...] -- +--pages [--file=]file [options] [...] -- Run qpdf --help=page-selection for details. .TP +.B --file \-\- source for pages +--file=file + +Specify the file for the current page operation. This is used +with --pages, --overlay, and --underlay and appears between the +option and the terminating --. Run qpdf --help=page-selection +for details. +.TP +.B --range \-\- page range +--range=numeric-range + +Specify the page range for the current page operation with +--pages. If omitted, all pages are selected. This is used +with --pages and appears between --pages and --. Run +qpdf --help=page-selection for details. +.TP .B --collate \-\- collate with --pages --collate[=n[,m,...]] @@ -754,12 +770,24 @@ should not be used except for compatibility testing. .SH PAGE-SELECTION (select pages from one or more files) Use the --pages option to select pages from multiple files. Usage: +qpdf in.pdf --pages --file=input-file \ + [--range=page-range] [--password=password] [...] -- out.pdf + +OR + qpdf in.pdf --pages input-file [--password=password] [page-range] \ [...] -- out.pdf Between --pages and the -- that terminates pages option, repeat the following: +--file=filename [--range=page-range] [--password=password] [options] + +For compatibility, the file and range can be specified +positionally. qpdf versions prior to 11.9.0 +require --password=password to immediately follow the filename. In +the older syntax, repeat the following: + filename [--password=password] [page-range] Document-level information, such as outlines, tags, etc., is taken @@ -789,7 +817,7 @@ pages from b.pdf, and write the output to out.pdf. Document-level information from in.pdf is retained. Note the use of "." to refer to in.pdf. - qpdf in.pdf --pages . a.pdf b.pdf:even -- out.pdf + qpdf in.pdf --pages . a.pdf b.pdf 1-z:even -- out.pdf .IP \[bu] Take all the pages from a.pdf, all the pages from b.pdf in diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index df2555d6..8d242f43 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -249,7 +249,6 @@ QPDF not caching overridden objstm object 0 QPDFWriter original obj non-zero gen 0 QPDF_optimization indirect outlines 0 QPDF xref space 2 -QPDFJob pages range omitted at end 1 QPDFJob pages range omitted in middle 0 QPDFJob npages 0 QPDF already reserved object 0 @@ -690,3 +689,5 @@ QPDF skipping cache for known unchecked object 0 QPDF fix dangling triggered xref reconstruction 0 QPDFPageDocumentHelper flatten resources missing or invalid 0 QPDF recover xref stream 0 +QPDFJob misplaced page range 0 +QPDFJob duplicated range 0 diff --git a/qpdf/qtest/arg-parsing.test b/qpdf/qtest/arg-parsing.test index 297141d4..238f19cf 100644 --- a/qpdf/qtest/arg-parsing.test +++ b/qpdf/qtest/arg-parsing.test @@ -15,7 +15,7 @@ cleanup(); my $td = new TestDriver('arg-parsing'); -my $n_tests = 24; +my $n_tests = 25; $td->runtest("required argument", {$td->COMMAND => "qpdf --password minimal.pdf"}, @@ -62,14 +62,20 @@ $td->runtest("bad file detected as unclosed --pages", {$td->REGEXP => ".*pages options must be terminated with --.*", $td->EXIT_STATUS => 2}, $td->NORMALIZE_NEWLINES); -$td->runtest("misplaced pages password 1", - {$td->COMMAND => "qpdf --pages . 1 --password=z --"}, - {$td->REGEXP => ".*password must immediately follow a file name.*", +$td->runtest("misplaced pages range", + {$td->COMMAND => "qpdf --pages --range=1 . --password=z --"}, + {$td->REGEXP => ".*range must follow a file name.*", $td->EXIT_STATUS => 2}, $td->NORMALIZE_NEWLINES); -$td->runtest("misplaced pages password 2", +$td->runtest("duplicate pages range", + {$td->COMMAND => "qpdf --pages --file=." . + " --range=1 --range=2 . --password=z --"}, + {$td->REGEXP => ".*range already specified.*", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); +$td->runtest("misplaced pages password", {$td->COMMAND => "qpdf --pages --password=z . 1 --"}, - {$td->REGEXP => ".*password must immediately follow a file name.*", + {$td->REGEXP => ".*password must follow a file name.*", $td->EXIT_STATUS => 2}, $td->NORMALIZE_NEWLINES); $td->runtest("duplicated pages password", diff --git a/qpdf/qtest/merge-and-split.test b/qpdf/qtest/merge-and-split.test index a0b72ead..33935e71 100644 --- a/qpdf/qtest/merge-and-split.test +++ b/qpdf/qtest/merge-and-split.test @@ -21,9 +21,9 @@ my $n_tests = 28; # first time. The file 20-pages.pdf is specified with two different # paths to duplicate a page. my $pages_options = "--pages page-labels-and-outlines.pdf 1,3,5-7,z" . - " 20-pages.pdf --password=user z-15" . - " page-labels-and-outlines.pdf 12" . - " 20-pages.pdf 10" . + " --file=20-pages.pdf --range=z-15 --password=user" . + " page-labels-and-outlines.pdf --range=12" . + " --file=20-pages.pdf 10" . " ./20-pages.pdf --password=owner 10" . " minimal.pdf 1 --";