Allow --file and --range with --pages

Accept --file and --range as named parameters in additional to
allowing positional arguments. This is in preparation for adding
additional flags.
This commit is contained in:
Jay Berkenbilt 2024-01-09 11:41:18 -05:00
parent 20a134826c
commit 34f013c1be
20 changed files with 259 additions and 148 deletions

View File

@ -40,7 +40,9 @@ main(int argc, char* argv[])
->inputFile("in.pdf") ->inputFile("in.pdf")
->outputFile("out1.pdf") ->outputFile("out1.pdf")
->pages() ->pages()
->pageSpec(".", "1") // Prior to qpdf 11.9.0, call ->pageSpec(file, range, password)
->file(".")
->range("1")
->endPages() ->endPages()
->linearize() ->linearize()
->staticId() // for testing only ->staticId() // for testing only

View File

@ -243,6 +243,8 @@ class QPDFJob
public: public:
QPDF_DLL QPDF_DLL
Config* endPages(); 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 QPDF_DLL
PagesConfig* pageSpec( PagesConfig* pageSpec(
std::string const& filename, std::string const& range, char const* password = nullptr); std::string const& filename, std::string const& range, char const* password = nullptr);

View File

@ -5,3 +5,6 @@
// //
// clang-format off // 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);

View File

@ -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_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5
include/qpdf/auto_job_c_main.hh dbfc221d1533120d1aa9c361d8d2483dea5fcb1c0fd95144d98d305e64ed32a6 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 include/qpdf/auto_job_c_uo.hh ae21b69a1efa9333050f4833d465f6daff87e5b38e5106e49bbef5d4132e4ed1
job.yml 8ad309ac41520b34692bcf22fd5c2ef4810ff69562aed606bd57df7bf589bc43 job.yml 45761edeca048c7aa3e99340fcda1b6cd8efe4cc4c8b8a6628580243a4f49b57
libqpdf/qpdf/auto_job_decl.hh 1e8d73891bd1f0b5df5a5ca7405fb76d2d0fd024941b8c1b86489f1b5f9c5772 libqpdf/qpdf/auto_job_decl.hh 20d6affe1e260f5a1af4f1d82a820b933835440ff03020e877382da2e8dac6c6
libqpdf/qpdf/auto_job_help.hh 8c172913920a5273e04dc4d2059f2d78fc475960ac1738271357056beb02dd27 libqpdf/qpdf/auto_job_help.hh b19f8a7433c70df70b42f893a8964c801aa2bb78eecaa13cffab7add2ff81e0a
libqpdf/qpdf/auto_job_init.hh ea272fd6a6a5e4d23cabd70a7b7d5ecc543b6304008c656dcba2d353d378efc2 libqpdf/qpdf/auto_job_init.hh d74759d4999201a89dafddf6f0c855e9151bbf77ea91a92d6806510292950123
libqpdf/qpdf/auto_job_json_decl.hh 10ffb0d0e5ca09809a5d5d78f66dee393dfd2653a23441436465fd5ace151880 libqpdf/qpdf/auto_job_json_decl.hh 485540cde820987cfbed0aa7642a6416f2bd37164c8d4f2322f1381e73edf903
libqpdf/qpdf/auto_job_json_init.hh 9c3839877ab3b15a47e92086f0b5616da33fd4970538cc423d3b0a7ff33ce66a libqpdf/qpdf/auto_job_json_init.hh c8de8658daa82115b49bf084cebe1be0b8aea73f864a219d7349acc0982b56fe
libqpdf/qpdf/auto_job_schema.hh a882939b202d48ad1c0751c094f671ad7aad0fc04c3a4446ad83675db365c8a2 libqpdf/qpdf/auto_job_schema.hh 3e000b87255bee62ba29b794d67b2ae97cbbdfdb78be3878c51786913564901e
manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580
manual/cli.rst 43923fab0def5f76e537f03ba2f650270a1ae858a15747e355db2a6ea396f1a0 manual/cli.rst 408e17dc13d37befe34badc400dd34d3c283952d17ee3bf9a9d44898af3dabc7
manual/qpdf.1 cd335812d450ca83be3c7fe165299d3454b26b4999295f671d57e6b24f6ea7a1 manual/qpdf.1 c99d66833aee7a2294176875ca2e9ddf2531d4ab8fb282ea5c45cb82a5d028ea
manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b

View File

@ -195,9 +195,9 @@ options:
config: c_pages config: c_pages
prefix: Pages prefix: Pages
positional: true positional: true
manual:
- password
required_parameter: required_parameter:
file: file
range: page-range
password: password password: password
- table: encryption - table: encryption
config: c_main config: c_main
@ -436,9 +436,9 @@ json:
oi-min-width: oi-min-width:
optimize-images: optimize-images:
pages: pages:
- _file: "source for for pages" - file:
Pages.password: Pages.password:
_range: "page range" range:
remove-page-labels: remove-page-labels:
report-memory-usage: report-memory-usage:
rotate: rotate:

View File

@ -2342,6 +2342,9 @@ QPDFJob::handlePageSpecs(QPDF& pdf, std::vector<std::unique_ptr<QPDF>>& page_hea
if (page_spec.filename == ".") { if (page_spec.filename == ".") {
page_spec.filename = m->infilename.get(); page_spec.filename = m->infilename.get();
} }
if (page_spec.range.empty()) {
page_spec.range = "1-z";
}
} }
if (!m->keep_files_open_set) { if (!m->keep_files_open_set) {

View File

@ -34,9 +34,10 @@ namespace
std::shared_ptr<QPDFJob::UOConfig> c_uo; std::shared_ptr<QPDFJob::UOConfig> c_uo;
std::shared_ptr<QPDFJob::EncConfig> c_enc; std::shared_ptr<QPDFJob::EncConfig> c_enc;
std::vector<std::string> accumulated_args; std::vector<std::string> accumulated_args;
std::shared_ptr<char> pages_password{nullptr};
std::string user_password; std::string user_password;
std::string owner_password; std::string owner_password;
bool called_pages_file{false};
bool called_pages_range{false};
bool used_enc_password_args{false}; bool used_enc_password_args{false};
bool gave_input{false}; bool gave_input{false};
bool gave_output{false}; bool gave_output{false};
@ -236,82 +237,44 @@ ArgParser::argPages()
this->ap.selectOptionTable(O_PAGES); 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 void
ArgParser::argPagesPositional(std::string const& arg) ArgParser::argPagesPositional(std::string const& arg)
{ {
if (arg.empty()) { if (!called_pages_file) {
if (this->accumulated_args.empty()) { c_pages->file(arg);
return; called_pages_file = true;
} return;
} else {
this->accumulated_args.push_back(arg);
} }
if (called_pages_range) {
std::string file = this->accumulated_args.at(0); c_pages->file(arg);
char const* range_p = nullptr; called_pages_range = false;
return;
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();
} }
// This could be a range or a file. Try parsing.
// See if the user omitted the range entirely, in which case we assume "1-z". try {
std::string next_file; QUtil::parse_numrange(arg.c_str(), 0);
if (range_p == nullptr) { c_pages->range(arg);
if (arg.empty()) { called_pages_range = true;
// The filename or password was the last argument } catch (std::runtime_error& e1) {
QTC::TC("qpdf", "QPDFJob pages range omitted at end", this->pages_password ? 0 : 1); // 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 { } else {
// We need to accumulate some more arguments // Give the range error
return; usage(e1.what());
} }
} else { c_pages->file(arg);
try { called_pages_range = false;
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);
} }
} }
void void
ArgParser::argEndPages() ArgParser::argEndPages()
{ {
argPagesPositional("");
c_pages->endPages(); c_pages->endPages();
c_pages = nullptr; c_pages = nullptr;
} }

View File

@ -968,6 +968,45 @@ QPDFJob::PagesConfig::pageSpec(
return this; 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::UOConfig> std::shared_ptr<QPDFJob::UOConfig>
QPDFJob::Config::overlay() QPDFJob::Config::overlay()
{ {

View File

@ -467,25 +467,17 @@ Handlers::endPagesArray()
void void
Handlers::beginPages(JSON j) Handlers::beginPages(JSON j)
{ {
std::string file;
std::string range("1-z");
std::string password;
bool file_seen = false; bool file_seen = false;
bool password_seen = false; j.forEachDictItem([&](std::string const& key, JSON const& value) {
j.forEachDictItem([&](std::string const& key, JSON value) {
if (key == "file") { if (key == "file") {
file_seen = value.getString(file); std::string v;
} else if (key == "range") { file_seen = value.getString(v);
value.getString(range);
} else if (key == "password") {
password_seen = value.getString(password);
} }
}); });
if (!file_seen) { if (!file_seen) {
QTC::TC("qpdf", "QPDFJob json pages no file"); QTC::TC("qpdf", "QPDFJob json pages no file");
usage("file is required in page specification"); usage("file is required in page specification");
} }
this->c_pages->pageSpec(file, range, password_seen ? password.c_str() : nullptr);
} }
void void
@ -494,25 +486,10 @@ Handlers::endPages()
// nothing needed // nothing needed
} }
void
Handlers::setupPagesFile()
{
// handled in beginPages
ignoreItem();
}
void void
Handlers::setupPagesPassword() Handlers::setupPagesPassword()
{ {
// handled in beginPages addParameter([this](char const* p) { c_pages->password(p); });
ignoreItem();
}
void
Handlers::setupPagesRange()
{
// handled in beginPages
ignoreItem();
} }
void void

View File

@ -31,7 +31,6 @@ void argReplaceInput();
void argSetPageLabels(); void argSetPageLabels();
void argUnderlay(); void argUnderlay();
void argPagesPositional(std::string const&); void argPagesPositional(std::string const&);
void argPagesPassword(std::string const&);
void argEndPages(); void argEndPages();
void argEncPositional(std::string const&); void argEncPositional(std::string const&);
void argEncUserPassword(std::string const&); void argEncUserPassword(std::string const&);

View File

@ -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 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. 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. 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,...]] ap.addOptionHelp("--collate", "modification", "collate with --pages", R"(--collate[=n[,m,...]]
Collate rather than concatenate pages specified with --pages. 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 1 and continuing sequentially until the end of the document. For
additional examples, please consult the manual. additional examples, please consult the manual.
)"); )");
}
static void add_help_5(QPDFArgParser& ap)
{
ap.addHelpTopic("encryption", "create encrypted files", R"(Create encrypted files. Usage: ap.addHelpTopic("encryption", "create encrypted files", R"(Create encrypted files. Usage:
--encrypt \ --encrypt \
@ -520,9 +537,6 @@ ap.addOptionHelp("--user-password", "encryption", "specify user password", R"(--
Set the user password of the encrypted file. 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 ap.addOptionHelp("--owner-password", "encryption", "specify owner password", R"(--owner-password=owner-password
Set the owner password of the encrypted file. 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: 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] \ qpdf in.pdf --pages input-file [--password=password] [page-range] \
[...] -- out.pdf [...] -- out.pdf
Between --pages and the -- that terminates pages option, repeat Between --pages and the -- that terminates pages option, repeat
the following: 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] filename [--password=password] [page-range]
Document-level information, such as outlines, tags, etc., is taken 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 information from in.pdf is retained. Note the use of "." to refer
to in.pdf. 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 - 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 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. 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 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 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 with the page range syntax. The page range may be omitted
if --repeat is used. 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 ap.addOptionHelp("--repeat", "overlay-underlay", "overlay/underlay pages to repeat", R"(--repeat=page-range
Specify pages from the overlay/underlay that are repeated after 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 the options in this section are specified, no output file may be
given. 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: 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 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 2: the file is not encrypted
3: the file is encrypted, and correct password (if any) has been supplied 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 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 aspects of the file, and write information about the file to
standard output. Note that qpdf does not perform any validation 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 output a JSON object with the same keys and with values
containing descriptive text. 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 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 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 be shown in the "objects" key of the JSON output. Otherwise, all
objects will be shown. 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} 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 When used with --json, this option controls whether streams in

View File

@ -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.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.registerOptionTable("pages", b(&ArgParser::argEndPages));
this->ap.addPositional(p(&ArgParser::argPagesPositional)); 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.registerOptionTable("encryption", b(&ArgParser::argEndEncryption));
this->ap.addPositional(p(&ArgParser::argEncPositional)); this->ap.addPositional(p(&ArgParser::argEncPositional));
this->ap.addRequiredParameter("user-password", p(&ArgParser::argEncUserPassword), "user_password"); this->ap.addRequiredParameter("user-password", p(&ArgParser::argEncUserPassword), "user_password");

View File

@ -41,9 +41,7 @@ void beginPagesArray(JSON);
void endPagesArray(); void endPagesArray();
void beginPages(JSON); void beginPages(JSON);
void endPages(); void endPages();
void setupPagesFile();
void setupPagesPassword(); void setupPagesPassword();
void setupPagesRange();
void beginSetPageLabelsArray(JSON); void beginSetPageLabelsArray(JSON);
void endSetPageLabelsArray(); void endSetPageLabelsArray();
void setupSetPageLabels(); void setupSetPageLabels();

View File

@ -402,13 +402,13 @@ pushKey("pages");
beginArray(bindJSON(&Handlers::beginPagesArray), bindBare(&Handlers::endPagesArray)); // .pages[] beginArray(bindJSON(&Handlers::beginPagesArray), bindBare(&Handlers::endPagesArray)); // .pages[]
beginDict(bindJSON(&Handlers::beginPages), bindBare(&Handlers::endPages)); // .pages beginDict(bindJSON(&Handlers::beginPages), bindBare(&Handlers::endPages)); // .pages
pushKey("file"); pushKey("file");
setupPagesFile(); addParameter([this](std::string const& p) { c_pages->file(p); });
popHandler(); // key: file popHandler(); // key: file
pushKey("password"); pushKey("password");
setupPagesPassword(); setupPagesPassword();
popHandler(); // key: password popHandler(); // key: password
pushKey("range"); pushKey("range");
setupPagesRange(); addParameter([this](std::string const& p) { c_pages->range(p); });
popHandler(); // key: range popHandler(); // key: range
popHandler(); // array: .pages[] popHandler(); // array: .pages[]
popHandler(); // key: pages popHandler(); // key: pages

View File

@ -140,7 +140,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({
"optimizeImages": "use efficient compression for images", "optimizeImages": "use efficient compression for images",
"pages": [ "pages": [
{ {
"file": "source for for pages", "file": "source for pages",
"password": "password for encrypted file", "password": "password for encrypted file",
"range": "page range" "range": "page range"
} }

View File

@ -1388,7 +1388,7 @@ PDF, causing the PDF to render differently from the original. See also
Related Options Related Options
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
.. qpdf:option:: --pages file [--password=password] [page-range] [...] -- .. qpdf:option:: --pages [--file=]file [options] [...] --
.. help: begin page selection .. help: begin page selection
@ -1403,6 +1403,38 @@ Related Options
See also :qpdf:ref:`--split-pages`, :qpdf:ref:`--collate`, See also :qpdf:ref:`--split-pages`, :qpdf:ref:`--collate`,
:ref:`page-ranges`. :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,...]] .. qpdf:option:: --collate[=n[,m,...]]
.. help: collate with --pages .. help: collate with --pages
@ -2424,12 +2456,24 @@ Page Selection
Use the --pages option to select pages from multiple files. Usage: 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] \ qpdf in.pdf --pages input-file [--password=password] [page-range] \
[...] -- out.pdf [...] -- out.pdf
Between --pages and the -- that terminates pages option, repeat Between --pages and the -- that terminates pages option, repeat
the following: 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] filename [--password=password] [page-range]
Document-level information, such as outlines, tags, etc., is taken 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 information from in.pdf is retained. Note the use of "." to refer
to in.pdf. 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 - 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 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 split and merge PDF files by selecting pages from one or more input
files. files.
Usage: :samp:`qpdf {in.pdf} --pages input-file [--password={password}] [{page-range}] [...] -- {out.pdf}` ::
Between ``--pages`` and the ``--`` that terminates pages option, qpdf primary-input.pdf \
repeat the following: --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: 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. - The password option is needed only for password-protected files.
If you specify the same file more than once, you only need to supply If you specify the same file more than once, you only need to supply
the password the first time. 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 - 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` :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 - Scan a document with double-sided printing by scanning the fronts
into :file:`odd.pdf` and the backs into :file:`even.pdf`. Collate 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 qpdf --collate odd.pdf --pages . even.pdf -- all.pdf
OR OR
qpdf --collate --empty --pages odd.pdf even.pdf -- all.pdf 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 - When collating, any number of files and page ranges can be
specified. If any file has fewer pages, that file is just skipped specified. If any file has fewer pages, that file is just skipped

View File

@ -409,10 +409,26 @@ the PDF, causing the PDF to render differently from the original.
Related Options: Related Options:
.TP .TP
.B --pages \-\- begin page selection .B --pages \-\- begin page selection
--pages file [--password=password] [page-range] [...] -- --pages [--file=]file [options] [...] --
Run qpdf --help=page-selection for details. Run qpdf --help=page-selection for details.
.TP .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 .B --collate \-\- collate with --pages
--collate[=n[,m,...]] --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) .SH PAGE-SELECTION (select pages from one or more files)
Use the --pages option to select pages from multiple files. Usage: 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] \ qpdf in.pdf --pages input-file [--password=password] [page-range] \
[...] -- out.pdf [...] -- out.pdf
Between --pages and the -- that terminates pages option, repeat Between --pages and the -- that terminates pages option, repeat
the following: 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] filename [--password=password] [page-range]
Document-level information, such as outlines, tags, etc., is taken 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 information from in.pdf is retained. Note the use of "." to refer
to in.pdf. 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] .IP \[bu]
Take all the pages from a.pdf, all the pages from b.pdf in Take all the pages from a.pdf, all the pages from b.pdf in

View File

@ -249,7 +249,6 @@ QPDF not caching overridden objstm object 0
QPDFWriter original obj non-zero gen 0 QPDFWriter original obj non-zero gen 0
QPDF_optimization indirect outlines 0 QPDF_optimization indirect outlines 0
QPDF xref space 2 QPDF xref space 2
QPDFJob pages range omitted at end 1
QPDFJob pages range omitted in middle 0 QPDFJob pages range omitted in middle 0
QPDFJob npages 0 QPDFJob npages 0
QPDF already reserved object 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 QPDF fix dangling triggered xref reconstruction 0
QPDFPageDocumentHelper flatten resources missing or invalid 0 QPDFPageDocumentHelper flatten resources missing or invalid 0
QPDF recover xref stream 0 QPDF recover xref stream 0
QPDFJob misplaced page range 0
QPDFJob duplicated range 0

View File

@ -15,7 +15,7 @@ cleanup();
my $td = new TestDriver('arg-parsing'); my $td = new TestDriver('arg-parsing');
my $n_tests = 24; my $n_tests = 25;
$td->runtest("required argument", $td->runtest("required argument",
{$td->COMMAND => "qpdf --password minimal.pdf"}, {$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->REGEXP => ".*pages options must be terminated with --.*",
$td->EXIT_STATUS => 2}, $td->EXIT_STATUS => 2},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
$td->runtest("misplaced pages password 1", $td->runtest("misplaced pages range",
{$td->COMMAND => "qpdf --pages . 1 --password=z --"}, {$td->COMMAND => "qpdf --pages --range=1 . --password=z --"},
{$td->REGEXP => ".*password must immediately follow a file name.*", {$td->REGEXP => ".*range must follow a file name.*",
$td->EXIT_STATUS => 2}, $td->EXIT_STATUS => 2},
$td->NORMALIZE_NEWLINES); $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->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->EXIT_STATUS => 2},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
$td->runtest("duplicated pages password", $td->runtest("duplicated pages password",

View File

@ -21,9 +21,9 @@ my $n_tests = 28;
# first time. The file 20-pages.pdf is specified with two different # first time. The file 20-pages.pdf is specified with two different
# paths to duplicate a page. # paths to duplicate a page.
my $pages_options = "--pages page-labels-and-outlines.pdf 1,3,5-7,z" . my $pages_options = "--pages page-labels-and-outlines.pdf 1,3,5-7,z" .
" 20-pages.pdf --password=user z-15" . " --file=20-pages.pdf --range=z-15 --password=user" .
" page-labels-and-outlines.pdf 12" . " page-labels-and-outlines.pdf --range=12" .
" 20-pages.pdf 10" . " --file=20-pages.pdf 10" .
" ./20-pages.pdf --password=owner 10" . " ./20-pages.pdf --password=owner 10" .
" minimal.pdf 1 --"; " minimal.pdf 1 --";