diff --git a/README-maintainer b/README-maintainer index 88e81461..9f3c79a0 100644 --- a/README-maintainer +++ b/README-maintainer @@ -215,6 +215,14 @@ CODING RULES HOW TO ADD A COMMAND-LINE ARGUMENT +Quick reminder: + +* Add an entry to the top half of job.yml for the command-line + argument +* Add an entry to the bottom half of job.yml for the job JSON field +* Add documentation for the new option to cli.rst +* Implement the QPDFJob::Config method in QPDFJob_config.cc. + QPDFJob is documented in three places: * This section provides a quick reminder for how to add a command-line diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index bbbb896e..5cba93a8 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -110,6 +110,28 @@ class QPDF void processInputSource(std::shared_ptr, char const* password = 0); + // Create a PDF from an input source that contains JSON as written + // by qpdf --json (version 2 or higher). The JSON must be a + // complete representation of a PDF. See "QPDF JSON Format" in the + // manual for details. + QPDF_DLL + void + createFromJSON(std::string const& json_file); + QPDF_DLL + void + createFromJSON(std::shared_ptr); + + // Update a PDF from an input source that contains JSON in the + // same format as is written by qpdf --json (version 2 or higher). + // Objects in the PDF and not in the JSON are not modified. See + // "QPDF JSON Format" in the manual for details. + QPDF_DLL + void + updateFromJSON(std::string const& json_file); + QPDF_DLL + void + updateFromJSON(std::shared_ptr); + // Close or otherwise release the input source. Once this has been // called, no other methods of qpdf can be called safely except // for getWarnings and anyWarnings(). After this has been called, diff --git a/include/qpdf/QPDFJob.hh b/include/qpdf/QPDFJob.hh index da8d3fcb..a7d86337 100644 --- a/include/qpdf/QPDFJob.hh +++ b/include/qpdf/QPDFJob.hh @@ -323,6 +323,8 @@ class QPDFJob Config* outputFile(std::string const& filename); QPDF_DLL Config* replaceInput(); + QPDF_DLL + Config* createFromJson(std::string const& filename); QPDF_DLL std::shared_ptr copyAttachmentsFrom(); @@ -674,6 +676,8 @@ class QPDFJob bool check_requires_password; std::shared_ptr infilename; std::shared_ptr outfilename; + std::string create_from_json; + std::string update_from_json; }; std::shared_ptr m; }; diff --git a/include/qpdf/auto_job_c_main.hh b/include/qpdf/auto_job_c_main.hh index 42e30a70..cc1bf469 100644 --- a/include/qpdf/auto_job_c_main.hh +++ b/include/qpdf/auto_job_c_main.hh @@ -68,6 +68,7 @@ QPDF_DLL Config* rotate(std::string const& parameter); QPDF_DLL Config* showAttachment(std::string const& parameter); QPDF_DLL Config* showObject(std::string const& parameter); QPDF_DLL Config* jsonStreamPrefix(std::string const& parameter); +QPDF_DLL Config* updateFromJson(std::string const& parameter); QPDF_DLL Config* collate(std::string const& parameter); QPDF_DLL Config* collate(); QPDF_DLL Config* splitPages(std::string const& parameter); diff --git a/job.sums b/job.sums index 8a387e14..3814155a 100644 --- a/job.sums +++ b/job.sums @@ -3,15 +3,15 @@ generate_auto_job 0514289f2deb3bf7c1a6e85ef7d99ad120321ef5a6fe49d76c5274c6a658d3 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 -include/qpdf/auto_job_c_main.hh 50214d1583d0384e70ce7c91d6bb92c58f8cc125490a680681cfffe6455a1dce +include/qpdf/auto_job_c_main.hh 178a0c98c80d53036910ec67165dbc3902aa8da857de8a0df52911f005918c54 include/qpdf/auto_job_c_pages.hh b3cc0f21029f6d89efa043dcdbfa183cb59325b6506001c18911614fe8e568ec include/qpdf/auto_job_c_uo.hh ae21b69a1efa9333050f4833d465f6daff87e5b38e5106e49bbef5d4132e4ed1 -job.yml c046a750e0cf6889b920484ab937bcb999be55273d77b263cb227b82006fbb36 -libqpdf/qpdf/auto_job_decl.hh 74df4d7fdbdf51ecd0d58ce1e9844bb5525b9adac5a45f7c9a787ecdda2868df -libqpdf/qpdf/auto_job_help.hh e9b37d33bfcbf165bfba21b6778df3f356b904a961bfae68f9638b85142a87e8 -libqpdf/qpdf/auto_job_init.hh 423157a51fa470fb45d6e341cc3fc8f044b5344f06f86475b37302610c7d8afd -libqpdf/qpdf/auto_job_json_decl.hh 06caa46eaf71db8a50c046f91866baa8087745a9474319fb7c86d92634cc8297 -libqpdf/qpdf/auto_job_json_init.hh eaed8624a1a394c75a3e298e1c31015146211e240d710509eb627fc711a387a6 -libqpdf/qpdf/auto_job_schema.hh a9971c82c9821a5ec620ccc003bcb3383c054e45658b50fa559b5855e694ed1a +job.yml a95b2446066293f409b36032a0ee411dbe570a7a94f5fd295048d009215f993f +libqpdf/qpdf/auto_job_decl.hh 833bde9c1f8fc17b914f16498e26d9d1315361645b4ac5c50c1f830a76618ca7 +libqpdf/qpdf/auto_job_help.hh 38bf89067ca7dc244e4e697c598ba0ba8a827600a78b9195fc69f0ac1663f3a2 +libqpdf/qpdf/auto_job_init.hh f0ffab312430b232a7288b8443382ed859021e6ad6ed2c8c9a4dbbd2b33e2aa7 +libqpdf/qpdf/auto_job_json_decl.hh 81d09d4b82b2e042a64246ed1d7a187bdc83b671b45e7b8ee60ad37c0c11e9a7 +libqpdf/qpdf/auto_job_json_init.hh 2fcdae08365abe351d2dfb6a823e2b3af27a6510632de23cabef6cefa5dfb199 +libqpdf/qpdf/auto_job_schema.hh 1a80be3b8d97e9b5a55b8aa45a4b312668b1687eab6f038c4ee5f4662ab71997 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst f1bbf59ce4fdb5a6d29fc2470788eee321423dd946984fc2e6f3a904fe5137c1 +manual/cli.rst 4550dd1b459721d8ef9affa7c9c07f351a06379498ec1291f702f1a5994b6d84 diff --git a/job.yml b/job.yml index c76244cf..b4fb7370 100644 --- a/job.yml +++ b/job.yml @@ -86,6 +86,7 @@ options: - underlay - empty - replace-input + - create-from-json positional: true bare: - add-attachment @@ -163,6 +164,8 @@ options: show-attachment: attachment show-object: trailer json-stream-prefix: stream-file-prefix + create-from-json: qpdf-json file + update-from-json: qpdf-json file required_choices: compress-streams: yn decode-level: decode_level @@ -278,6 +281,7 @@ json: main.password: password-file: empty: + create-from-json: # output _outputFile: "output filename" replace-input: @@ -360,6 +364,7 @@ json: json-stream-prefix: to-json: # other options + update-from-json: allow-weak-crypto: keep-files-open: keep-files-open-threshold: diff --git a/libqpdf/CMakeLists.txt b/libqpdf/CMakeLists.txt index aa7b1fd0..1777bb08 100644 --- a/libqpdf/CMakeLists.txt +++ b/libqpdf/CMakeLists.txt @@ -97,6 +97,7 @@ set(libqpdf_SOURCES QPDF_Stream.cc QPDF_String.cc QPDF_encryption.cc + QPDF_json.cc QPDF_linearization.cc QPDF_optimization.cc QPDF_pages.cc diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index 55bebdcb..41e57c58 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -553,6 +553,13 @@ QPDFJob::run() if (m->check_is_encrypted || m->check_requires_password) { return; } + + // If we are updating from JSON, this has to be done first before + // other options may cause transformations to the input. + if (!this->m->update_from_json.empty()) { + pdf.updateFromJSON(this->m->update_from_json); + } + bool other_warnings = false; std::vector> page_heap; if (!m->page_specs.empty()) { @@ -1937,7 +1944,11 @@ QPDFJob::doProcessOnce( auto pdf = std::make_shared(); setQPDFOptions(*pdf); if (empty) { - pdf->emptyPDF(); + if (!this->m->create_from_json.empty()) { + pdf->createFromJSON(this->m->create_from_json); + } else { + pdf->emptyPDF(); + } } else { fn(pdf.get(), password); } diff --git a/libqpdf/QPDFJob_argv.cc b/libqpdf/QPDFJob_argv.cc index a6fb5df7..3058d166 100644 --- a/libqpdf/QPDFJob_argv.cc +++ b/libqpdf/QPDFJob_argv.cc @@ -100,6 +100,13 @@ ArgParser::argReplaceInput() this->gave_output = true; } +void +ArgParser::argCreateFromJson(std::string const& arg) +{ + c_main->createFromJson(arg); + this->gave_input = true; +} + void ArgParser::argVersion() { diff --git a/libqpdf/QPDFJob_config.cc b/libqpdf/QPDFJob_config.cc index f69a8137..1aa680e8 100644 --- a/libqpdf/QPDFJob_config.cc +++ b/libqpdf/QPDFJob_config.cc @@ -25,12 +25,14 @@ QPDFJob::Config::emptyInput() { if (o.m->infilename == 0) { // Various places in QPDFJob.cc know that the empty string for - // infile means empty. This means that passing "" as the - // argument to inputFile, or equivalently using "" as a - // positional command-line argument would be the same as - // --empty. This probably isn't worth blocking or coding - // around, but it would be better if we had a tighter way of - // knowing that the input file is empty. + // infile means empty. We set it to something other than a + // null pointer as an indication that some input source has + // been specified. The --create-from-json option also sets + // infilename to empty. This approach means that passing "" as + // the argument to inputFile in job JSON, or equivalently + // using "" as a positional command-line argument would be the + // same as --empty. This probably isn't worth blocking or + // coding around. o.m->infilename = QUtil::make_shared_cstr(""); } else { usage("empty input can't be used" @@ -293,6 +295,23 @@ QPDFJob::Config::toJson() return this; } +QPDFJob::Config* +QPDFJob::Config::createFromJson(std::string const& parameter) +{ + // See comments in emptyInput() about setting infilename to the + // empty string. + o.m->infilename = QUtil::make_shared_cstr(""); + o.m->create_from_json = parameter; + return this; +} + +QPDFJob::Config* +QPDFJob::Config::updateFromJson(std::string const& parameter) +{ + o.m->update_from_json = parameter; + return this; +} + QPDFJob::Config* QPDFJob::Config::testJsonSchema() { diff --git a/libqpdf/QPDFJob_json.cc b/libqpdf/QPDFJob_json.cc index fcdeb666..9538153a 100644 --- a/libqpdf/QPDFJob_json.cc +++ b/libqpdf/QPDFJob_json.cc @@ -250,6 +250,12 @@ Handlers::setupEmpty() addBare([this]() { c_main->emptyInput(); }); } +void +Handlers::setupCreateFromJson() +{ + addParameter([this](char const* p) { c_main->createFromJson(p); }); +} + void Handlers::setupOutputFile() { diff --git a/libqpdf/QPDF_json.cc b/libqpdf/QPDF_json.cc new file mode 100644 index 00000000..908615b6 --- /dev/null +++ b/libqpdf/QPDF_json.cc @@ -0,0 +1,27 @@ +#include + +#include + +void +QPDF::createFromJSON(std::string const& json_file) +{ + createFromJSON(std::make_shared(json_file.c_str())); +} + +void +QPDF::createFromJSON(std::shared_ptr) +{ + // QXXXQ +} + +void +QPDF::updateFromJSON(std::string const& json_file) +{ + updateFromJSON(std::make_shared(json_file.c_str())); +} + +void +QPDF::updateFromJSON(std::shared_ptr) +{ + // QXXXQ +} diff --git a/libqpdf/qpdf/auto_job_decl.hh b/libqpdf/qpdf/auto_job_decl.hh index 02f0ca91..8c656986 100644 --- a/libqpdf/qpdf/auto_job_decl.hh +++ b/libqpdf/qpdf/auto_job_decl.hh @@ -28,6 +28,7 @@ void argOverlay(); void argPages(); void argReplaceInput(); void argUnderlay(); +void argCreateFromJson(std::string const&); void argPagesPositional(std::string const&); void argPagesPassword(std::string const&); void argEndPages(); diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index d04f48a6..e17412d0 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/libqpdf/qpdf/auto_job_help.hh @@ -836,6 +836,21 @@ name as the prefix for stream data files. Whatever is given here will be appended with -nnn to create the name of the file that will contain the data for the stream stream in object nnn. )"); +ap.addOptionHelp("--create-from-json", "json", "create PDF from qpdf JSON", R"(--create-from-json=qpdf-json-file + +Create a PDF file from the prior output of qpdf --json. See the +"QPDF JSON Format" section of the manual for information about +how to use this option. +)"); +ap.addOptionHelp("--update-from-json", "json", "update a PDF from qpdf JSON", R"(--update-from-json=qpdf-json-file + +Update a PDF file from a JSON file. Please see the "QPDF JSON +Format" section of the manual for information about how to use +this option. +)"); +} +static void add_help_8(QPDFArgParser& ap) +{ ap.addHelpTopic("testing", "options for testing or debugging", R"(The options below are useful when writing automated test code that includes files created by qpdf or when testing qpdf itself. )"); @@ -843,9 +858,6 @@ ap.addOptionHelp("--static-id", "testing", "use a fixed document ID", R"(Use a f testing only. Never use it for production files. See also qpdf --help=--deterministic-id. )"); -} -static void add_help_8(QPDFArgParser& ap) -{ ap.addOptionHelp("--static-aes-iv", "testing", "use a fixed AES vector", R"(Use a static initialization vector for AES-CBC. This is intended for testing only so that output files can be reproducible. Never use it for production files. This option is not secure since it diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index f3ec1db6..9446811f 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -104,6 +104,8 @@ this->ap.addRequiredParameter("rotate", [this](std::string const& x){c_main->rot this->ap.addRequiredParameter("show-attachment", [this](std::string const& x){c_main->showAttachment(x);}, "attachment"); this->ap.addRequiredParameter("show-object", [this](std::string const& x){c_main->showObject(x);}, "trailer"); this->ap.addRequiredParameter("json-stream-prefix", [this](std::string const& x){c_main->jsonStreamPrefix(x);}, "stream-file-prefix"); +this->ap.addRequiredParameter("create-from-json", p(&ArgParser::argCreateFromJson), "qpdf-json file"); +this->ap.addRequiredParameter("update-from-json", [this](std::string const& x){c_main->updateFromJson(x);}, "qpdf-json file"); this->ap.addOptionalParameter("collate", [this](std::string const& x){c_main->collate(x);}); this->ap.addOptionalParameter("split-pages", [this](std::string const& x){c_main->splitPages(x);}); this->ap.addChoices("compress-streams", [this](std::string const& x){c_main->compressStreams(x);}, true, yn_choices); diff --git a/libqpdf/qpdf/auto_job_json_decl.hh b/libqpdf/qpdf/auto_job_json_decl.hh index f02dc657..8160ca5a 100644 --- a/libqpdf/qpdf/auto_job_json_decl.hh +++ b/libqpdf/qpdf/auto_job_json_decl.hh @@ -8,6 +8,7 @@ void setupInputFile(); void setupPassword(); void setupEmpty(); +void setupCreateFromJson(); void setupOutputFile(); void setupReplaceInput(); void beginEncrypt(JSON); diff --git a/libqpdf/qpdf/auto_job_json_init.hh b/libqpdf/qpdf/auto_job_json_init.hh index cb349efc..ee124d0d 100644 --- a/libqpdf/qpdf/auto_job_json_init.hh +++ b/libqpdf/qpdf/auto_job_json_init.hh @@ -30,6 +30,9 @@ popHandler(); // key: passwordFile pushKey("empty"); setupEmpty(); popHandler(); // key: empty +pushKey("createFromJson"); +setupCreateFromJson(); +popHandler(); // key: createFromJson pushKey("outputFile"); setupOutputFile(); popHandler(); // key: outputFile @@ -262,6 +265,9 @@ popHandler(); // key: jsonStreamPrefix pushKey("toJson"); addBare([this]() { c_main->toJson(); }); popHandler(); // key: toJson +pushKey("updateFromJson"); +addParameter([this](std::string const& p) { c_main->updateFromJson(p); }); +popHandler(); // key: updateFromJson pushKey("allowWeakCrypto"); addBare([this]() { c_main->allowWeakCrypto(); }); popHandler(); // key: allowWeakCrypto diff --git a/libqpdf/qpdf/auto_job_schema.hh b/libqpdf/qpdf/auto_job_schema.hh index 666b7260..0b9e29a9 100644 --- a/libqpdf/qpdf/auto_job_schema.hh +++ b/libqpdf/qpdf/auto_job_schema.hh @@ -3,6 +3,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ "password": "password for encrypted file", "passwordFile": "read password from a file", "empty": "use empty file as input", + "createFromJson": "create PDF from qpdf JSON", "outputFile": "output filename", "replaceInput": "overwrite input with output", "qdf": "enable viewing PDF code in a text editor", @@ -87,6 +88,7 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ "jsonStreamData": "how to handle streams in json output", "jsonStreamPrefix": "prefix for json stream data files", "toJson": "serialize to JSON", + "updateFromJson": "update a PDF from qpdf JSON", "allowWeakCrypto": "allow insecure cryptographic algorithms", "keepFilesOpen": "manage keeping multiple files open", "keepFilesOpenThreshold": "set threshold for keepFilesOpen", diff --git a/manual/cli.rst b/manual/cli.rst index 92194e78..57a164cb 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -3270,6 +3270,31 @@ Related Options :samp:`-{nnn}` to create the name of the file that will contain the data for the stream stream in object :samp:`{nnn}`. +.. qpdf:option:: --create-from-json=qpdf-json-file + + .. help: create PDF from qpdf JSON + + Create a PDF file from the prior output of qpdf --json. See the + "QPDF JSON Format" section of the manual for information about + how to use this option. + + This option creates a PDF file from the previous output of ``qpdf + --json`` that includes stream data and information about all + objects. For information about converting between PDF and JSON, + please see :ref:`qpdf-json`. + +.. qpdf:option:: --update-from-json=qpdf-json-file + + .. help: update a PDF from qpdf JSON + + Update a PDF file from a JSON file. Please see the "QPDF JSON + Format" section of the manual for information about how to use + this option. + + This option updates a PDF file from a qpdf JSON file. For a + information about how to use this option, please see + :ref:`qpdf-json`. + .. _test-options: Options for Testing or Debugging diff --git a/manual/json.rst b/manual/json.rst index ef6bed96..a3922051 100644 --- a/manual/json.rst +++ b/manual/json.rst @@ -18,6 +18,13 @@ files programmatically from the command-line in languages that can't call or link with the qpdf library directly. Note that stream data can be extracted from PDF files using other qpdf command-line options. +.. _qpdf-json: + +QPDF JSON Format +---------------- + +QXXXQ Write this. + .. _json-guarantees: JSON Guarantees