aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2022-05-15 18:34:27 +0200
committerJay Berkenbilt <ejb@ql.org>2022-05-16 19:41:40 +0200
commit7fa5d1773b599df51df21658dff53f5b66fdbba8 (patch)
tree1cd7a6fc7b9efa3b1b0fb436f0e5b05c23a1f03c
parent8d42eb2632ca5c2ae1200160e78116505b57f6b0 (diff)
downloadqpdf-7fa5d1773b599df51df21658dff53f5b66fdbba8.tar.zst
Implement top-level qpdf json parsing
-rw-r--r--TODO7
-rw-r--r--include/qpdf/QPDF.hh27
-rw-r--r--libqpdf/QPDF_json.cc197
-rw-r--r--qpdf/qpdf.testcov9
-rw-r--r--qpdf/qtest/qpdf-json.test50
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-json-version1.json73
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-json-version1.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-json-version2.json73
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-json-version2.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-object-key.json75
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-object-key.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-pdf-version1.json73
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-pdf-version1.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-pdf-version2.json73
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-pdf-version2.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-no-json-version.json72
-rw-r--r--qpdf/qtest/qpdf/qjson-no-json-version.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-no-pdf-version.json72
-rw-r--r--qpdf/qtest/qpdf/qjson-no-pdf-version.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-no-qpdf-object.json3
-rw-r--r--qpdf/qtest/qpdf/qjson-no-qpdf-object.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-object-not-dict.json68
-rw-r--r--qpdf/qtest/qpdf/qjson-object-not-dict.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-objects-not-dict.json7
-rw-r--r--qpdf/qtest/qpdf/qjson-objects-not-dict.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-stream-dict-not-dict.json13
-rw-r--r--qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-stream-not-dict.json11
-rw-r--r--qpdf/qtest/qpdf/qjson-stream-not-dict.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-top-level-array.json1
-rw-r--r--qpdf/qtest/qpdf/qjson-top-level-array.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-top-level-scalar.json1
-rw-r--r--qpdf/qtest/qpdf/qjson-top-level-scalar.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-trailer-not-dict.json70
-rw-r--r--qpdf/qtest/qpdf/qjson-trailer-not-dict.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-trailer-stream.json70
-rw-r--r--qpdf/qtest/qpdf/qjson-trailer-stream.out1
37 files changed, 1053 insertions, 8 deletions
diff --git a/TODO b/TODO
index ef5d6d88..afc68a9c 100644
--- a/TODO
+++ b/TODO
@@ -54,6 +54,13 @@ Soon: Break ground on "Document-level work"
Output JSON v2
==============
+Try to never flatten pages tree. Make sure we do something reasonable
+with pages tree repair. The problem is that if pages tree repair is
+done as a side effect of running --json, the qpdf part of the json may
+contain object numbers that aren't there. Maybe we need to indicate
+whether pages tree repair has been done in the json, but this would
+have to be known early in parsing, which is a problem.
+
General things to remember:
* Make sure all the information from --check and other informational
diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh
index 61efa4ed..2e24b261 100644
--- a/include/qpdf/QPDF.hh
+++ b/include/qpdf/QPDF.hh
@@ -998,6 +998,7 @@ class QPDF
class JSONReactor: public JSON::Reactor
{
public:
+ JSONReactor(QPDF&, bool must_be_complete);
virtual ~JSONReactor() = default;
virtual void dictionaryStart() override;
virtual void arrayStart() override;
@@ -1006,6 +1007,32 @@ class QPDF
virtual bool
dictionaryItem(std::string const& key, JSON const& value) override;
virtual bool arrayItem(JSON const& value) override;
+
+ private:
+ enum state_e {
+ st_initial,
+ st_top,
+ st_ignore,
+ st_qpdf,
+ st_objects_top,
+ st_trailer_top,
+ st_object_top,
+ st_stream,
+ st_object,
+ };
+
+ void containerStart();
+ void nestedState(std::string const& key, JSON const& value, state_e);
+
+ QPDF& pdf;
+ bool must_be_complete;
+ bool saw_qpdf;
+ bool saw_json_version;
+ bool saw_pdf_version;
+ bool saw_trailer;
+ state_e state;
+ state_e next_state;
+ std::vector<state_e> state_stack;
};
friend class JSONReactor;
diff --git a/libqpdf/QPDF_json.cc b/libqpdf/QPDF_json.cc
index 316c9935..2f74a673 100644
--- a/libqpdf/QPDF_json.cc
+++ b/libqpdf/QPDF_json.cc
@@ -1,42 +1,218 @@
#include <qpdf/QPDF.hh>
#include <qpdf/FileInputSource.hh>
+#include <qpdf/QTC.hh>
+#include <qpdf/QUtil.hh>
+#include <regex>
+
+namespace
+{
+ class JSONExc: public std::runtime_error
+ {
+ public:
+ JSONExc(JSON const& value, std::string const& msg) :
+ std::runtime_error(
+ "offset " + QUtil::uint_to_string(value.getStart()) + ": " +
+ msg)
+ {
+ }
+ };
+} // namespace
+
+static std::regex PDF_VERSION_RE("^\\d+\\.\\d+$");
+static std::regex OBJ_KEY_RE("^obj:(\\d+) (\\d+) R$");
+
+QPDF::JSONReactor::JSONReactor(QPDF& pdf, bool must_be_complete) :
+ pdf(pdf),
+ must_be_complete(must_be_complete),
+ saw_qpdf(false),
+ saw_json_version(false),
+ saw_pdf_version(false),
+ saw_trailer(false),
+ state(st_initial),
+ next_state(st_top)
+{
+ state_stack.push_back(st_initial);
+}
+
+void
+QPDF::JSONReactor::containerStart()
+{
+ state_stack.push_back(state);
+ state = next_state;
+}
void
QPDF::JSONReactor::dictionaryStart()
{
- // QXXXXQ
+ containerStart();
+ // QXXXQ
}
void
QPDF::JSONReactor::arrayStart()
{
- // QXXXXQ
+ containerStart();
+ if (state == st_top) {
+ QTC::TC("qpdf", "QPDF_json top-level array");
+ throw std::runtime_error("QPDF JSON must be a dictionary");
+ }
+ // QXXXQ
}
void
QPDF::JSONReactor::containerEnd(JSON const& value)
{
- // QXXXXQ
+ state = state_stack.back();
+ state_stack.pop_back();
+ if (state == st_initial) {
+ if (!this->saw_qpdf) {
+ QTC::TC("qpdf", "QPDF_json missing qpdf");
+ throw std::runtime_error("\"qpdf\" object was not seen");
+ }
+ if (!this->saw_json_version) {
+ QTC::TC("qpdf", "QPDF_json missing json version");
+ throw std::runtime_error("\"qpdf.jsonversion\" was not seen");
+ }
+ if (must_be_complete && !this->saw_pdf_version) {
+ QTC::TC("qpdf", "QPDF_json missing pdf version");
+ throw std::runtime_error("\"qpdf.pdfversion\" was not seen");
+ }
+ if (must_be_complete && !this->saw_trailer) {
+ /// QTC::TC("qpdf", "QPDF_json missing trailer");
+ throw std::runtime_error("\"qpdf.objects.trailer\" was not seen");
+ }
+ }
+
+ // QXXXQ
}
void
QPDF::JSONReactor::topLevelScalar()
{
- // QXXXXQ
+ QTC::TC("qpdf", "QPDF_json top-level scalar");
+ throw std::runtime_error("QPDF JSON must be a dictionary");
+}
+
+void
+QPDF::JSONReactor::nestedState(
+ std::string const& key, JSON const& value, state_e next)
+{
+ // Use this method when the next state is for processing a nested
+ // dictionary.
+ if (!value.isDictionary()) {
+ throw JSONExc(value, "\"" + key + "\" must be a dictionary");
+ }
+ this->next_state = next;
}
bool
QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
{
- // QXXXXQ
+ if (state == st_ignore) {
+ // ignore
+ } else if (state == st_top) {
+ if (key == "qpdf") {
+ this->saw_qpdf = true;
+ nestedState(key, value, st_qpdf);
+ } else {
+ // Ignore all other fields for forward compatibility.
+ // Don't use nestedState since this can be any type.
+ next_state = st_ignore;
+ }
+ } else if (state == st_qpdf) {
+ if (key == "jsonversion") {
+ this->saw_json_version = true;
+ std::string v;
+ if (!(value.getNumber(v) && (v == "2"))) {
+ QTC::TC("qpdf", "QPDF_json bad json version");
+ throw JSONExc(value, "only JSON version 2 is supported");
+ }
+ } else if (key == "pdfversion") {
+ this->saw_pdf_version = true;
+ bool version_okay = false;
+ std::string v;
+ if (value.getString(v)) {
+ std::smatch m;
+ if (std::regex_match(v, m, PDF_VERSION_RE)) {
+ version_okay = true;
+ this->pdf.m->pdf_version = v;
+ }
+ }
+ if (!version_okay) {
+ QTC::TC("qpdf", "QPDF_json bad pdf version");
+ throw JSONExc(value, "invalid PDF version (must be x.y)");
+ }
+ } else if (key == "objects") {
+ nestedState(key, value, st_objects_top);
+ } else {
+ // ignore unknown keys for forward compatibility
+ }
+ } else if (state == st_objects_top) {
+ std::smatch m;
+ if (key == "trailer") {
+ this->saw_trailer = true;
+ nestedState(key, value, st_trailer_top);
+ // QXXXQ
+ } else if (std::regex_match(key, m, OBJ_KEY_RE)) {
+ nestedState(key, value, st_object_top);
+ // QXXXQ
+ } else {
+ QTC::TC("qpdf", "QPDF_json bad object key");
+ throw JSONExc(
+ value, "object key should be \"trailer\" or \"obj:n n R\"");
+ }
+ } else if (state == st_object_top) {
+ if (key == "value") {
+ // Don't use nestedState since this can have any type.
+ next_state = st_object;
+ // QXXXQ
+ } else if (key == "stream") {
+ nestedState(key, value, st_stream);
+ // QXXXQ
+ } else {
+ // Ignore unknown keys for forward compatibility
+ }
+ } else if (state == st_trailer_top) {
+ if (key == "value") {
+ // The trailer must be a dictionary, so we can use nestedState.
+ nestedState("trailer.value", value, st_object);
+ // QXXXQ
+ } else if (key == "stream") {
+ QTC::TC("qpdf", "QPDF_json trailer stream");
+ throw JSONExc(value, "the trailer may not be a stream");
+ } else {
+ // Ignore unknown keys for forward compatibility
+ }
+ } else if (state == st_stream) {
+ if (key == "dict") {
+ // Since a stream dictionary must be a dictionary, we can
+ // use nestedState to transition to st_value.
+ nestedState("stream.dict", value, st_object);
+ // QXXXQ
+ } else if (key == "data") {
+ // QXXXQ
+ } else if (key == "datafile") {
+ // QXXXQ
+ } else {
+ // Ignore unknown keys for forward compatibility.
+ next_state = st_ignore;
+ }
+ } else if (state == st_object) {
+ // QXXXQ
+ } else {
+ throw std::logic_error(
+ "QPDF_json: unknown state " + QUtil::int_to_string(state));
+ }
+
+ // QXXXQ
return true;
}
bool
QPDF::JSONReactor::arrayItem(JSON const& value)
{
- // QXXXXQ
+ // QXXXQ
return true;
}
@@ -65,7 +241,12 @@ QPDF::updateFromJSON(std::shared_ptr<InputSource> is)
}
void
-QPDF::importJSON(std::shared_ptr<InputSource>, bool must_be_complete)
+QPDF::importJSON(std::shared_ptr<InputSource> is, bool must_be_complete)
{
- // QXXXQ
+ JSONReactor reactor(*this, must_be_complete);
+ try {
+ JSON::parse(*is, &reactor);
+ } catch (std::runtime_error& e) {
+ throw std::runtime_error(is->getName() + ": " + e.what());
+ }
}
diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov
index 4225fc64..69bd350b 100644
--- a/qpdf/qpdf.testcov
+++ b/qpdf/qpdf.testcov
@@ -650,3 +650,12 @@ QPDFJob json encrypt duplicate key length 0
QPDFJob json encrypt missing password 0
QPDFJob json pages no file 0
qpdf-c called qpdf_empty_pdf 0
+QPDF_json missing qpdf 0
+QPDF_json missing json version 0
+QPDF_json missing pdf version 0
+QPDF_json top-level scalar 0
+QPDF_json bad json version 0
+QPDF_json bad pdf version 0
+QPDF_json top-level array 0
+QPDF_json bad object key 0
+QPDF_json trailer stream 0
diff --git a/qpdf/qtest/qpdf-json.test b/qpdf/qtest/qpdf-json.test
new file mode 100644
index 00000000..39078a87
--- /dev/null
+++ b/qpdf/qtest/qpdf-json.test
@@ -0,0 +1,50 @@
+#!/usr/bin/env perl
+require 5.008;
+use warnings;
+use strict;
+
+unshift(@INC, '.');
+require qpdf_test_helpers;
+
+chdir("qpdf") or die "chdir testdir failed: $!\n";
+
+require TestDriver;
+
+cleanup();
+
+my $td = new TestDriver('qpdf-json');
+
+my $n_tests = 0;
+
+my @badfiles = (
+ 'no-qpdf-object',
+ 'no-json-version',
+ 'no-pdf-version',
+ 'top-level-scalar',
+ 'bad-json-version1',
+ 'bad-json-version2',
+ 'bad-pdf-version1',
+ 'bad-pdf-version2',
+ 'top-level-array',
+ 'objects-not-dict',
+ 'bad-object-key',
+ 'object-not-dict',
+ 'stream-not-dict',
+ 'stream-dict-not-dict',
+ 'trailer-not-dict',
+ 'trailer-stream',
+ );
+
+$n_tests += scalar(@badfiles);
+
+foreach my $f (@badfiles)
+{
+ $td->runtest("bad: $f",
+ {$td->COMMAND =>
+ "qpdf --create-from-json=qjson-$f.json a.pdf"},
+ {$td->FILE => "qjson-$f.out", $td->EXIT_STATUS => 2},
+ $td->NORMALIZE_NEWLINES);
+}
+
+cleanup();
+$td->report($n_tests);
diff --git a/qpdf/qtest/qpdf/qjson-bad-json-version1.json b/qpdf/qtest/qpdf/qjson-bad-json-version1.json
new file mode 100644
index 00000000..78591a42
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-json-version1.json
@@ -0,0 +1,73 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 16059,
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-json-version1.out b/qpdf/qtest/qpdf/qjson-bad-json-version1.out
new file mode 100644
index 00000000..1c921c2b
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-json-version1.out
@@ -0,0 +1 @@
+qpdf: qjson-bad-json-version1.json: offset 98: only JSON version 2 is supported
diff --git a/qpdf/qtest/qpdf/qjson-bad-json-version2.json b/qpdf/qtest/qpdf/qjson-bad-json-version2.json
new file mode 100644
index 00000000..20843caf
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-json-version2.json
@@ -0,0 +1,73 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": "potato",
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-json-version2.out b/qpdf/qtest/qpdf/qjson-bad-json-version2.out
new file mode 100644
index 00000000..756df7fe
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-json-version2.out
@@ -0,0 +1 @@
+qpdf: qjson-bad-json-version2.json: offset 98: only JSON version 2 is supported
diff --git a/qpdf/qtest/qpdf/qjson-bad-object-key.json b/qpdf/qtest/qpdf/qjson-bad-object-key.json
new file mode 100644
index 00000000..77441846
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-object-key.json
@@ -0,0 +1,75 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "potato": {
+ },
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-object-key.out b/qpdf/qtest/qpdf/qjson-bad-object-key.out
new file mode 100644
index 00000000..91edfc8f
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-object-key.out
@@ -0,0 +1 @@
+qpdf: qjson-bad-object-key.json: offset 181: object key should be "trailer" or "obj:n n R"
diff --git a/qpdf/qtest/qpdf/qjson-bad-pdf-version1.json b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.json
new file mode 100644
index 00000000..eccaeeca
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.json
@@ -0,0 +1,73 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "potato",
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out
new file mode 100644
index 00000000..61331957
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out
@@ -0,0 +1 @@
+qpdf: qjson-bad-pdf-version1.json: offset 119: invalid PDF version (must be x.y)
diff --git a/qpdf/qtest/qpdf/qjson-bad-pdf-version2.json b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.json
new file mode 100644
index 00000000..04443eea
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.json
@@ -0,0 +1,73 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": [],
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out
new file mode 100644
index 00000000..2ef92df4
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out
@@ -0,0 +1 @@
+qpdf: qjson-bad-pdf-version2.json: offset 119: invalid PDF version (must be x.y)
diff --git a/qpdf/qtest/qpdf/qjson-no-json-version.json b/qpdf/qtest/qpdf/qjson-no-json-version.json
new file mode 100644
index 00000000..72f223ad
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-no-json-version.json
@@ -0,0 +1,72 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-no-json-version.out b/qpdf/qtest/qpdf/qjson-no-json-version.out
new file mode 100644
index 00000000..b745bc85
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-no-json-version.out
@@ -0,0 +1 @@
+qpdf: qjson-no-json-version.json: "qpdf.jsonversion" was not seen
diff --git a/qpdf/qtest/qpdf/qjson-no-pdf-version.json b/qpdf/qtest/qpdf/qjson-no-pdf-version.json
new file mode 100644
index 00000000..39f21be2
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-no-pdf-version.json
@@ -0,0 +1,72 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-no-pdf-version.out b/qpdf/qtest/qpdf/qjson-no-pdf-version.out
new file mode 100644
index 00000000..cd752750
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-no-pdf-version.out
@@ -0,0 +1 @@
+qpdf: qjson-no-pdf-version.json: "qpdf.pdfversion" was not seen
diff --git a/qpdf/qtest/qpdf/qjson-no-qpdf-object.json b/qpdf/qtest/qpdf/qjson-no-qpdf-object.json
new file mode 100644
index 00000000..994a97e8
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-no-qpdf-object.json
@@ -0,0 +1,3 @@
+{
+ "potato": "salad"
+}
diff --git a/qpdf/qtest/qpdf/qjson-no-qpdf-object.out b/qpdf/qtest/qpdf/qjson-no-qpdf-object.out
new file mode 100644
index 00000000..765d1bc5
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-no-qpdf-object.out
@@ -0,0 +1 @@
+qpdf: qjson-no-qpdf-object.json: "qpdf" object was not seen
diff --git a/qpdf/qtest/qpdf/qjson-object-not-dict.json b/qpdf/qtest/qpdf/qjson-object-not-dict.json
new file mode 100644
index 00000000..c0831c74
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-object-not-dict.json
@@ -0,0 +1,68 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "obj:1 0 R": "potato",
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-object-not-dict.out b/qpdf/qtest/qpdf/qjson-object-not-dict.out
new file mode 100644
index 00000000..5ddeb7f2
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-object-not-dict.out
@@ -0,0 +1 @@
+qpdf: qjson-object-not-dict.json: offset 184: "obj:1 0 R" must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-objects-not-dict.json b/qpdf/qtest/qpdf/qjson-objects-not-dict.json
new file mode 100644
index 00000000..0e16294a
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-objects-not-dict.json
@@ -0,0 +1,7 @@
+{
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.7",
+ "objects": false
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-objects-not-dict.out b/qpdf/qtest/qpdf/qjson-objects-not-dict.out
new file mode 100644
index 00000000..7504428c
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-objects-not-dict.out
@@ -0,0 +1 @@
+qpdf: qjson-objects-not-dict.json: offset 77: "objects" must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.json b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.json
new file mode 100644
index 00000000..19bd08b1
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.json
@@ -0,0 +1,13 @@
+{
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.7",
+ "objects": {
+ "obj:1 0 R": {
+ "stream": {
+ "dict": "quack"
+ }
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out
new file mode 100644
index 00000000..0341766e
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out
@@ -0,0 +1 @@
+qpdf: qjson-stream-dict-not-dict.json: offset 137: "stream.dict" must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-stream-not-dict.json b/qpdf/qtest/qpdf/qjson-stream-not-dict.json
new file mode 100644
index 00000000..cef86c95
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-stream-not-dict.json
@@ -0,0 +1,11 @@
+{
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.7",
+ "objects": {
+ "obj:1 0 R": {
+ "stream": 3
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-stream-not-dict.out b/qpdf/qtest/qpdf/qjson-stream-not-dict.out
new file mode 100644
index 00000000..e1f85a29
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-stream-not-dict.out
@@ -0,0 +1 @@
+qpdf: qjson-stream-not-dict.json: offset 118: "stream" must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-top-level-array.json b/qpdf/qtest/qpdf/qjson-top-level-array.json
new file mode 100644
index 00000000..2e2c15f8
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-top-level-array.json
@@ -0,0 +1 @@
+["potato"]
diff --git a/qpdf/qtest/qpdf/qjson-top-level-array.out b/qpdf/qtest/qpdf/qjson-top-level-array.out
new file mode 100644
index 00000000..b8a90532
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-top-level-array.out
@@ -0,0 +1 @@
+qpdf: qjson-top-level-array.json: QPDF JSON must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-top-level-scalar.json b/qpdf/qtest/qpdf/qjson-top-level-scalar.json
new file mode 100644
index 00000000..a9d93e43
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-top-level-scalar.json
@@ -0,0 +1 @@
+"potato"
diff --git a/qpdf/qtest/qpdf/qjson-top-level-scalar.out b/qpdf/qtest/qpdf/qjson-top-level-scalar.out
new file mode 100644
index 00000000..75649cf1
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-top-level-scalar.out
@@ -0,0 +1 @@
+qpdf: qjson-top-level-scalar.json: QPDF JSON must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-trailer-not-dict.json b/qpdf/qtest/qpdf/qjson-trailer-not-dict.json
new file mode 100644
index 00000000..69782074
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-trailer-not-dict.json
@@ -0,0 +1,70 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": false,
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-trailer-not-dict.out b/qpdf/qtest/qpdf/qjson-trailer-not-dict.out
new file mode 100644
index 00000000..bec6c7ca
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-trailer-not-dict.out
@@ -0,0 +1 @@
+qpdf: qjson-trailer-not-dict.json: offset 1326: "trailer.value" must be a dictionary
diff --git a/qpdf/qtest/qpdf/qjson-trailer-stream.json b/qpdf/qtest/qpdf/qjson-trailer-stream.json
new file mode 100644
index 00000000..74891e5b
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-trailer-stream.json
@@ -0,0 +1,70 @@
+{
+ "version": 2,
+ "parameters": {
+ "decodelevel": "none"
+ },
+ "qpdf": {
+ "jsonversion": 2,
+ "pdfversion": "1.3",
+ "maxobjectid": 6,
+ "objects": {
+ "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",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "stream": {},
+ }
+ }
+ }
+}
diff --git a/qpdf/qtest/qpdf/qjson-trailer-stream.out b/qpdf/qtest/qpdf/qjson-trailer-stream.out
new file mode 100644
index 00000000..267ecf08
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-trailer-stream.out
@@ -0,0 +1 @@
+qpdf: qjson-trailer-stream.json: offset 1327: the trailer may not be a stream