From f1a2d3160a1dbaf735cce597d0f6f40e76f7f223 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Thu, 8 Sep 2022 15:57:33 -0400 Subject: Add JSON v2 support to C API --- ChangeLog | 6 ++ TODO | 6 -- include/qpdf/qpdf-c.h | 75 ++++++++++++++++++++-- libqpdf/qpdf-c.cc | 88 ++++++++++++++++++++++++++ manual/release-notes.rst | 4 ++ qpdf/qpdf-ctest.c | 119 ++++++++++++++++++++++++++++++++++- qpdf/qtest/qpdf-json.test | 53 ++++++++++++++++ qpdf/qtest/qpdf/minimal-update.json | 17 +++++ qpdf/qtest/qpdf/minimal.json | 74 ++++++++++++++++++++++ qpdf/qtest/qpdf/qpdf-ctest-42-43.pdf | Bin 0 -> 799 bytes qpdf/qtest/qpdf/qpdf-ctest-44-45.pdf | Bin 0 -> 798 bytes qpdf/qtest/qpdf/qpdf-ctest-46.json | 74 ++++++++++++++++++++++ qpdf/qtest/qpdf/qpdf-ctest-47-4 | 5 ++ qpdf/qtest/qpdf/qpdf-ctest-47.json | 25 ++++++++ 14 files changed, 535 insertions(+), 11 deletions(-) create mode 100644 qpdf/qtest/qpdf/minimal-update.json create mode 100644 qpdf/qtest/qpdf/minimal.json create mode 100644 qpdf/qtest/qpdf/qpdf-ctest-42-43.pdf create mode 100644 qpdf/qtest/qpdf/qpdf-ctest-44-45.pdf create mode 100644 qpdf/qtest/qpdf/qpdf-ctest-46.json create mode 100644 qpdf/qtest/qpdf/qpdf-ctest-47-4 create mode 100644 qpdf/qtest/qpdf/qpdf-ctest-47.json diff --git a/ChangeLog b/ChangeLog index 751b1aed..b7e793a8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2022-09-08 Jay Berkenbilt + * Added new functions to the C API to support qpdf JSON: + qpdf_create_from_json_file, qpdf_create_from_json_data, + qpdf_update_from_json_file, qpdf_update_from_json_data, and + qpdf_write_json. Examples can be found in qpdf-ctest.c (in the + source tree), tests 42 through 47. + * Add QPDFObjectHandle::isDestroyed() to test whether an indirect object was from a QPDF that has been destroyed. diff --git a/TODO b/TODO index 0ed0a24c..5f3cd99a 100644 --- a/TODO +++ b/TODO @@ -8,12 +8,6 @@ Always Next ==== -Before Release: - -* Support json v2 in the C API. At a minimum, write_json, - create_from_json, and update_from_json need to be there and should - take the same kinds of functions as the C API for logger. - Pending changes: * Consider also exposing a way to set a new logger and to get the diff --git a/include/qpdf/qpdf-c.h b/include/qpdf/qpdf-c.h index 4b40f068..434c4ede 100644 --- a/include/qpdf/qpdf-c.h +++ b/include/qpdf/qpdf-c.h @@ -257,8 +257,6 @@ extern "C" { QPDF_DLL QPDF_ERROR_CODE qpdf_check_pdf(qpdf_data qpdf); - /* READ FUNCTIONS */ - /* READ PARAMETER FUNCTIONS -- must be called before qpdf_read */ QPDF_DLL @@ -267,6 +265,10 @@ extern "C" { QPDF_DLL void qpdf_set_attempt_recovery(qpdf_data qpdf, QPDF_BOOL value); + /* PROCESS FUNCTIONS */ + + /* This functions process a PDF or JSON input source. */ + /* Calling qpdf_read causes processFile to be called in the C++ * API. Basic parsing is performed, but data from the file is * only read as needed. For files without passwords, pass a null @@ -297,8 +299,40 @@ extern "C" { QPDF_DLL QPDF_ERROR_CODE qpdf_empty_pdf(qpdf_data qpdf); - /* Read functions below must be called after qpdf_read or - * qpdf_read_memory. */ + /* Create a PDF from a JSON file. This calls createFromJSON in the + * C++ API. + */ + QPDF_DLL + QPDF_ERROR_CODE + qpdf_create_from_json_file(qpdf_data qpdf, char const* filename); + + /* Create a PDF from JSON data in a null-terminated string. This + * calls createFromJSON in the C++ API. + */ + QPDF_DLL + QPDF_ERROR_CODE + qpdf_create_from_json_data( + qpdf_data qpdf, char const* buffer, unsigned long long size); + + /* JSON UPDATE FUNCTIONS */ + + /* Update a QPDF object from a JSON file or buffer. These + * functions call updateFromJSON. One of the other processing + * functions has to be called first so that the QPDF object is + * initialized with PDF data. + */ + QPDF_DLL + QPDF_ERROR_CODE + qpdf_update_from_json_file(qpdf_data qpdf, char const* filename); + QPDF_DLL + QPDF_ERROR_CODE + qpdf_update_from_json_data( + qpdf_data qpdf, char const* buffer, unsigned long long size); + + /* READ FUNCTIONS */ + + /* Read functions below must be called after qpdf_read or any of + * the other functions that process a PDF. */ /* * NOTE: Functions that return char* are returning a pointer to an @@ -371,6 +405,39 @@ extern "C" { QPDF_DLL QPDF_BOOL qpdf_allow_modify_all(qpdf_data qpdf); + /* JSON WRITE FUNCTIONS */ + + /* This function serializes the PDF to JSON. This calls writeJSON + * from the C++ API. + * + * - version: the JSON version, currently must be 2 + * - fn: a function that will be called with blocks of JSON data; + * will be called with data, a length, and the value of the + * udata parameter to this function + * - udata: will be passed as the third argument to fn with each + * call; use this for your own tracking or pass a null pointer + * if you don't need it + * - For decode_level, json_stream_data, file_prefix, and + * wanted_objects, see comments in QPDF.hh. For this API, + * wanted_objects should be a null-terminated array of + * null-terminated strings. Pass a null pointer if you want all + * objects. + */ + + /* Function should return 0 on success. */ + typedef int (*qpdf_write_fn_t)(char const* data, size_t len, void* udata); + + QPDF_DLL + QPDF_ERROR_CODE qpdf_write_json( + qpdf_data qpdf, + int version, + qpdf_write_fn_t fn, + void* udata, + enum qpdf_stream_decode_level_e decode_level, + enum qpdf_json_stream_data_e json_stream_data, + char const* file_prefix, + char const* const* wanted_objects); + /* WRITE FUNCTIONS */ /* Set up for writing. No writing is actually performed until the diff --git a/libqpdf/qpdf-c.cc b/libqpdf/qpdf-c.cc index b5bae727..a8ae8102 100644 --- a/libqpdf/qpdf-c.cc +++ b/libqpdf/qpdf-c.cc @@ -2,8 +2,10 @@ #include +#include #include #include +#include #include #include #include @@ -2029,3 +2031,89 @@ qpdf_remove_page(qpdf_data qpdf, qpdf_oh page) auto p = qpdf_oh_item_internal(qpdf, page); return trap_errors(qpdf, [&p](qpdf_data q) { q->qpdf->removePage(p); }); } + +QPDF_ERROR_CODE +qpdf_create_from_json_file(qpdf_data qpdf, char const* filename) +{ + QPDF_ERROR_CODE status = QPDF_SUCCESS; + qpdf->filename = filename; + status = trap_errors( + qpdf, [](qpdf_data q) { q->qpdf->createFromJSON(q->filename); }); + return status; +} + +QPDF_ERROR_CODE +qpdf_create_from_json_data( + qpdf_data qpdf, char const* buffer, unsigned long long size) +{ + QPDF_ERROR_CODE status = QPDF_SUCCESS; + qpdf->filename = "json buffer"; + qpdf->buffer = buffer; + qpdf->size = size; + auto b = + new Buffer(QUtil::unsigned_char_pointer(buffer), QIntC::to_size(size)); + auto is = std::make_shared(qpdf->filename, b, true); + status = + trap_errors(qpdf, [&is](qpdf_data q) { q->qpdf->createFromJSON(is); }); + return status; +} + +QPDF_ERROR_CODE +qpdf_update_from_json_file(qpdf_data qpdf, char const* filename) +{ + QPDF_ERROR_CODE status = QPDF_SUCCESS; + status = trap_errors( + qpdf, [filename](qpdf_data q) { q->qpdf->updateFromJSON(filename); }); + return status; +} + +QPDF_ERROR_CODE +qpdf_update_from_json_data( + qpdf_data qpdf, char const* buffer, unsigned long long size) +{ + QPDF_ERROR_CODE status = QPDF_SUCCESS; + auto b = + new Buffer(QUtil::unsigned_char_pointer(buffer), QIntC::to_size(size)); + auto is = std::make_shared(qpdf->filename, b, true); + status = + trap_errors(qpdf, [&is](qpdf_data q) { q->qpdf->updateFromJSON(is); }); + return status; +} + +QPDF_ERROR_CODE +qpdf_write_json( + qpdf_data qpdf, + int version, + qpdf_write_fn_t fn, + void* udata, + enum qpdf_stream_decode_level_e decode_level, + enum qpdf_json_stream_data_e json_stream_data, + char const* file_prefix, + char const* const* wanted_objects) +{ + QPDF_ERROR_CODE status = QPDF_SUCCESS; + auto p = std::make_shared("write_json", nullptr, fn, udata); + std::set wanted_objects_set; + if (wanted_objects) { + for (auto i = wanted_objects; *i; ++i) { + wanted_objects_set.insert(*i); + } + } + status = trap_errors( + qpdf, + [version, + p, + decode_level, + json_stream_data, + file_prefix, + &wanted_objects_set](qpdf_data q) { + q->qpdf->writeJSON( + version, + p.get(), + decode_level, + json_stream_data, + file_prefix, + wanted_objects_set); + }); + return status; +} diff --git a/manual/release-notes.rst b/manual/release-notes.rst index bf6a8301..e53d3bfb 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -40,6 +40,10 @@ For a detailed list of changes, please see the file - New C++ API calls: ``QPDF::writeJSON``, ``QPDF::createFromJSON``, ``QPDF::updateFromJSON`` + - New C API calls: ``qpdf_create_from_json_file``, + ``qpdf_create_from_json_data``, ``qpdf_update_from_json_file``, + ``qpdf_update_from_json_data``, and ``qpdf_write_json``. + - Complete documentation can be found at :ref:`json`. A comprehensive list of changes from version 1 to version 2 can be found at :ref:`json-v2-changes`. diff --git a/qpdf/qpdf-ctest.c b/qpdf/qpdf-ctest.c index 07d6ca9e..b562ed8b 100644 --- a/qpdf/qpdf-ctest.c +++ b/qpdf/qpdf-ctest.c @@ -133,6 +133,13 @@ count_progress(int percent, void* data) ++(*(int*)data); } +static int +write_to_file(char const* data, size_t size, void* udata) +{ + FILE* f = (FILE*)udata; + return fwrite(data, 1, size, f) != size; +} + static void test01( char const* infile, @@ -1458,7 +1465,7 @@ test41( char const* outfile, char const* xarg) { - /* Empty PDF -- infile is ignored*/ + /* Empty PDF -- infile is ignored */ assert(qpdf_empty_pdf(qpdf) == 0); qpdf_init_write(qpdf, outfile); qpdf_set_static_ID(qpdf, QPDF_TRUE); @@ -1466,6 +1473,110 @@ test41( report_errors(); } +static void +test42( + char const* infile, + char const* password, + char const* outfile, + char const* xarg) +{ + assert(qpdf_create_from_json_file(qpdf, infile) == QPDF_SUCCESS); + qpdf_init_write(qpdf, outfile); + qpdf_set_static_ID(qpdf, QPDF_TRUE); + qpdf_write(qpdf); + report_errors(); +} + +static void +test43( + char const* infile, + char const* password, + char const* outfile, + char const* xarg) +{ + char* buf = NULL; + unsigned long size = 0; + read_file_into_memory(infile, &buf, &size); + assert(qpdf_create_from_json_data(qpdf, buf, size) == QPDF_SUCCESS); + qpdf_init_write(qpdf, outfile); + qpdf_set_static_ID(qpdf, QPDF_TRUE); + qpdf_write(qpdf); + report_errors(); + free(buf); +} + +static void +test44( + char const* infile, + char const* password, + char const* outfile, + char const* xarg) +{ + assert(qpdf_read(qpdf, infile, password) == 0); + assert(qpdf_update_from_json_file(qpdf, xarg) == QPDF_SUCCESS); + qpdf_init_write(qpdf, outfile); + qpdf_set_static_ID(qpdf, QPDF_TRUE); + qpdf_write(qpdf); + report_errors(); +} + +static void +test45( + char const* infile, + char const* password, + char const* outfile, + char const* xarg) +{ + char* buf = NULL; + unsigned long size = 0; + read_file_into_memory(xarg, &buf, &size); + assert(qpdf_read(qpdf, infile, password) == 0); + assert(qpdf_update_from_json_data(qpdf, buf, size) == QPDF_SUCCESS); + qpdf_init_write(qpdf, outfile); + qpdf_set_static_ID(qpdf, QPDF_TRUE); + qpdf_write(qpdf); + report_errors(); + free(buf); +} + +static void +test46( + char const* infile, + char const* password, + char const* outfile, + char const* xarg) +{ + FILE* f = safe_fopen(outfile, "wb"); + assert(qpdf_read(qpdf, infile, password) == 0); + qpdf_write_json( + qpdf, 2, write_to_file, f, qpdf_dl_none, qpdf_sj_inline, "", NULL); + fclose(f); + report_errors(); +} + +static void +test47( + char const* infile, + char const* password, + char const* outfile, + char const* xarg) +{ + FILE* f = safe_fopen(outfile, "wb"); + assert(qpdf_read(qpdf, infile, password) == 0); + char const* wanted_objects[] = {"obj:4 0 R", "trailer", NULL}; + qpdf_write_json( + qpdf, + 2, + write_to_file, + f, + qpdf_dl_specialized, + qpdf_sj_file, + xarg, + wanted_objects); + fclose(f); + report_errors(); +} + int main(int argc, char* argv[]) { @@ -1542,6 +1653,12 @@ main(int argc, char* argv[]) : (n == 39) ? test39 : (n == 40) ? test40 : (n == 41) ? test41 + : (n == 42) ? test42 + : (n == 43) ? test43 + : (n == 44) ? test44 + : (n == 45) ? test45 + : (n == 46) ? test46 + : (n == 47) ? test47 : 0); if (fn == 0) { diff --git a/qpdf/qtest/qpdf-json.test b/qpdf/qtest/qpdf-json.test index 553a84e2..2867f8a7 100644 --- a/qpdf/qtest/qpdf-json.test +++ b/qpdf/qtest/qpdf-json.test @@ -288,5 +288,58 @@ $td->runtest("simple version of writeJSON", {$td->FILE => "minimal-write-json.json", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$n_tests += 13; +$td->runtest("C API create from json file", + {$td->COMMAND => "qpdf-ctest 42 minimal.json '' a.pdf"}, + {$td->STRING => "C test 42 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API create from file", + {$td->FILE => "a.pdf"}, + {$td->FILE => "qpdf-ctest-42-43.pdf"}); +$td->runtest("C API create from json buffer", + {$td->COMMAND => "qpdf-ctest 43 minimal.json '' a.pdf"}, + {$td->STRING => "C test 43 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API create from buffer", + {$td->FILE => "a.pdf"}, + {$td->FILE => "qpdf-ctest-42-43.pdf"}); +$td->runtest("C API update from json file", + {$td->COMMAND => + "qpdf-ctest 44 minimal.pdf '' a.pdf minimal-update.json"}, + {$td->STRING => "C test 44 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API update from file", + {$td->FILE => "a.pdf"}, + {$td->FILE => "qpdf-ctest-44-45.pdf"}); +$td->runtest("C API update from json buffer", + {$td->COMMAND => + "qpdf-ctest 45 minimal.pdf '' a.pdf minimal-update.json"}, + {$td->STRING => "C test 45 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API update from buffer", + {$td->FILE => "a.pdf"}, + {$td->FILE => "qpdf-ctest-44-45.pdf"}); +$td->runtest("C API write to JSON 1", + {$td->COMMAND => + "qpdf-ctest 46 minimal.pdf '' a.json"}, + {$td->STRING => "C test 46 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API write to JSON 1", + {$td->FILE => "a.json"}, + {$td->FILE => "qpdf-ctest-46.json"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("C API write to JSON 2", + {$td->COMMAND => + "qpdf-ctest 47 minimal.pdf '' a.json auto"}, + {$td->STRING => "C test 47 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API write to JSON 2", + {$td->FILE => "a.json"}, + {$td->FILE => "qpdf-ctest-47.json"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check C API write to JSON stream", + {$td->FILE => "auto-4"}, + {$td->FILE => "qpdf-ctest-47-4"}); + cleanup(); $td->report($n_tests); diff --git a/qpdf/qtest/qpdf/minimal-update.json b/qpdf/qtest/qpdf/minimal-update.json new file mode 100644 index 00000000..1bd9abc0 --- /dev/null +++ b/qpdf/qtest/qpdf/minimal-update.json @@ -0,0 +1,17 @@ +{ + "qpdf": [ + { + "jsonversion": 2, + "pushedinheritedpageresources": false, + "calledgetallpages": false + }, + { + "obj:4 0 R": { + "stream": { + "data": "QlQKICAvRjEgMjQgVGYKICA3MiA3MjAgVGQKICAoU2FsYWQpIFRqCkVUCg==", + "dict": {} + } + } + } + ] +} diff --git a/qpdf/qtest/qpdf/minimal.json b/qpdf/qtest/qpdf/minimal.json new file mode 100644 index 00000000..ae4a9faa --- /dev/null +++ b/qpdf/qtest/qpdf/minimal.json @@ -0,0 +1,74 @@ +{ + "qpdf": [ + { + "jsonversion": 2, + "pdfversion": "1.3", + "pushedinheritedpageresources": false, + "calledgetallpages": false, + "maxobjectid": 6 + }, + { + "obj:1 0 R": { + "value": { + "/Pages": "2 0 R", + "/Type": "/Catalog" + } + }, + "obj:2 0 R": { + "value": { + "/Count": 1, + "/Kids": [ + "3 0 R" + ], + "/Type": "/Pages" + } + }, + "obj:3 0 R": { + "value": { + "/Contents": "4 0 R", + "/MediaBox": [ + 0, + 0, + 612, + 792 + ], + "/Parent": "2 0 R", + "/Resources": { + "/Font": { + "/F1": "6 0 R" + }, + "/ProcSet": "5 0 R" + }, + "/Type": "/Page" + } + }, + "obj:4 0 R": { + "stream": { + "data": "QlQKICAvRjEgMjQgVGYKICA3MiA3MjAgVGQKICAoUG90YXRvKSBUagpFVAo=", + "dict": {} + } + }, + "obj:5 0 R": { + "value": [ + "/PDF", + "/Text" + ] + }, + "obj:6 0 R": { + "value": { + "/BaseFont": "/Helvetica", + "/Encoding": "/WinAnsiEncoding", + "/Name": "/F1", + "/Subtype": "/Type1", + "/Type": "/Font" + } + }, + "trailer": { + "value": { + "/Root": "1 0 R", + "/Size": 7 + } + } + } + ] +} diff --git a/qpdf/qtest/qpdf/qpdf-ctest-42-43.pdf b/qpdf/qtest/qpdf/qpdf-ctest-42-43.pdf new file mode 100644 index 00000000..b8c692ed Binary files /dev/null and b/qpdf/qtest/qpdf/qpdf-ctest-42-43.pdf differ diff --git a/qpdf/qtest/qpdf/qpdf-ctest-44-45.pdf b/qpdf/qtest/qpdf/qpdf-ctest-44-45.pdf new file mode 100644 index 00000000..f7f56fd9 Binary files /dev/null and b/qpdf/qtest/qpdf/qpdf-ctest-44-45.pdf differ diff --git a/qpdf/qtest/qpdf/qpdf-ctest-46.json b/qpdf/qtest/qpdf/qpdf-ctest-46.json new file mode 100644 index 00000000..ae4a9faa --- /dev/null +++ b/qpdf/qtest/qpdf/qpdf-ctest-46.json @@ -0,0 +1,74 @@ +{ + "qpdf": [ + { + "jsonversion": 2, + "pdfversion": "1.3", + "pushedinheritedpageresources": false, + "calledgetallpages": false, + "maxobjectid": 6 + }, + { + "obj:1 0 R": { + "value": { + "/Pages": "2 0 R", + "/Type": "/Catalog" + } + }, + "obj:2 0 R": { + "value": { + "/Count": 1, + "/Kids": [ + "3 0 R" + ], + "/Type": "/Pages" + } + }, + "obj:3 0 R": { + "value": { + "/Contents": "4 0 R", + "/MediaBox": [ + 0, + 0, + 612, + 792 + ], + "/Parent": "2 0 R", + "/Resources": { + "/Font": { + "/F1": "6 0 R" + }, + "/ProcSet": "5 0 R" + }, + "/Type": "/Page" + } + }, + "obj:4 0 R": { + "stream": { + "data": "QlQKICAvRjEgMjQgVGYKICA3MiA3MjAgVGQKICAoUG90YXRvKSBUagpFVAo=", + "dict": {} + } + }, + "obj:5 0 R": { + "value": [ + "/PDF", + "/Text" + ] + }, + "obj:6 0 R": { + "value": { + "/BaseFont": "/Helvetica", + "/Encoding": "/WinAnsiEncoding", + "/Name": "/F1", + "/Subtype": "/Type1", + "/Type": "/Font" + } + }, + "trailer": { + "value": { + "/Root": "1 0 R", + "/Size": 7 + } + } + } + ] +} diff --git a/qpdf/qtest/qpdf/qpdf-ctest-47-4 b/qpdf/qtest/qpdf/qpdf-ctest-47-4 new file mode 100644 index 00000000..b74381fa --- /dev/null +++ b/qpdf/qtest/qpdf/qpdf-ctest-47-4 @@ -0,0 +1,5 @@ +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET diff --git a/qpdf/qtest/qpdf/qpdf-ctest-47.json b/qpdf/qtest/qpdf/qpdf-ctest-47.json new file mode 100644 index 00000000..44f8b500 --- /dev/null +++ b/qpdf/qtest/qpdf/qpdf-ctest-47.json @@ -0,0 +1,25 @@ +{ + "qpdf": [ + { + "jsonversion": 2, + "pdfversion": "1.3", + "pushedinheritedpageresources": false, + "calledgetallpages": false, + "maxobjectid": 6 + }, + { + "obj:4 0 R": { + "stream": { + "datafile": "auto-4", + "dict": {} + } + }, + "trailer": { + "value": { + "/Root": "1 0 R", + "/Size": 7 + } + } + } + ] +} -- cgit v1.2.3-54-g00ecf