Merge branch 'jw' from #1146 into work

This commit is contained in:
Jay Berkenbilt 2024-02-17 14:15:48 -05:00
commit e362bce8e8
49 changed files with 2555 additions and 327 deletions

View File

@ -290,8 +290,11 @@ class JSON
QPDF_DLL
qpdf_offset_t getEnd() const;
// The following class does not form part of the public API and is for internal use only.
class Writer;
private:
static std::string encode_string(std::string const& utf8);
static void writeClose(Pipeline* p, bool first, size_t depth, char const* delimeter);
enum value_type_e {

View File

@ -1411,19 +1411,6 @@ class QPDF
// JSON import
void importJSON(std::shared_ptr<InputSource>, bool must_be_complete);
// JSON write
void writeJSONStream(
int version,
Pipeline* p,
bool& first,
std::string const& key,
QPDFObjectHandle&,
qpdf_stream_decode_level_e,
qpdf_json_stream_data_e,
std::string const& file_prefix);
void writeJSONObject(
int version, Pipeline* p, bool& first, std::string const& key, QPDFObjectHandle&);
// Type conversion helper methods
template <typename T>
static qpdf_offset_t

View File

@ -551,7 +551,6 @@ class QPDFJob
// JSON
void doJSON(QPDF& pdf, Pipeline*);
QPDFObjGen::set getWantedJSONObjects();
void doJSONObject(Pipeline* p, bool& first, std::string const& key, QPDFObjectHandle&);
void doJSONObjects(Pipeline* p, bool& first, QPDF& pdf);
void doJSONObjectinfo(Pipeline* p, bool& first, QPDF& pdf);
void doJSONPages(Pipeline* p, bool& first, QPDF& pdf);

View File

@ -1197,6 +1197,13 @@ class QPDFObjectHandle
QPDF_DLL
JSON getJSON(int json_version, bool dereference_indirect = false);
// Write the object encoded as JSON to a pipeline. This is equivalent to, but more efficient
// than, calling getJSON(json_version, dereference_indirect).write(p, depth). See the
// documentation for getJSON and JSON::write for further detail.
QPDF_DLL
void
writeJSON(int json_version, Pipeline* p, bool dereference_indirect = false, size_t depth = 0);
// Deprecated version uses v1 for backward compatibility.
// ABI: remove for qpdf 12
[[deprecated("Use getJSON(int version)")]] QPDF_DLL JSON
@ -1353,6 +1360,8 @@ class QPDFObjectHandle
return obj.get();
}
void writeJSON(int json_version, JSON::Writer& p, bool dereference_indirect = false);
private:
QPDF_Array* asArray();
QPDF_Bool* asBool();

View File

@ -1,5 +1,7 @@
#include <qpdf/JSON.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/BufferInputSource.hh>
#include <qpdf/Pl_Base64.hh>
#include <qpdf/Pl_Concatenate.hh>
@ -119,7 +121,7 @@ JSON::JSON_array::write(Pipeline* p, size_t depth) const
JSON::JSON_string::JSON_string(std::string const& utf8) :
JSON_value(vt_string),
utf8(utf8),
encoded(encode_string(utf8))
encoded(Writer::encode_string(utf8))
{
}
@ -211,7 +213,7 @@ JSON::unparse() const
}
std::string
JSON::encode_string(std::string const& str)
JSON::Writer::encode_string(std::string const& str)
{
static auto constexpr hexchars = "0123456789abcdef";
@ -279,7 +281,7 @@ JSON
JSON::addDictionaryMember(std::string const& key, JSON const& val)
{
if (auto* obj = m ? dynamic_cast<JSON_dictionary*>(m->value.get()) : nullptr) {
return obj->members[encode_string(key)] = val.m ? val : makeNull();
return obj->members[Writer::encode_string(key)] = val.m ? val : makeNull();
} else {
throw std::runtime_error("JSON::addDictionaryMember called on non-dictionary");
}

View File

@ -954,23 +954,6 @@ QPDFJob::getWantedJSONObjects()
return wanted_og;
}
void
QPDFJob::doJSONObject(Pipeline* p, bool& first, std::string const& key, QPDFObjectHandle& obj)
{
if (m->json_version == 1) {
JSON::writeDictionaryItem(p, first, key, obj.getJSON(1, true), 2);
} else {
auto j = JSON::makeDictionary();
if (obj.isStream()) {
j.addDictionaryMember("stream", JSON::makeDictionary())
.addDictionaryMember("dict", obj.getDict().getJSON(m->json_version, true));
} else {
j.addDictionaryMember("value", obj.getJSON(m->json_version, true));
}
JSON::writeDictionaryItem(p, first, key, j, 2);
}
}
void
QPDFJob::doJSONObjects(Pipeline* p, bool& first, QPDF& pdf)
{
@ -982,16 +965,17 @@ QPDFJob::doJSONObjects(Pipeline* p, bool& first, QPDF& pdf)
auto wanted_og = getWantedJSONObjects();
for (auto& obj: pdf.getAllObjects()) {
std::string key = obj.unparse();
if (m->json_version > 1) {
key = "obj:" + key;
}
if (all_objects || wanted_og.count(obj.getObjGen())) {
doJSONObject(p, first_object, key, obj);
JSON::writeDictionaryKey(p, first_object, obj.unparse(), 2);
obj.writeJSON(1, p, true, 2);
first_object = false;
}
}
if (all_objects || m->json_objects.count("trailer")) {
auto trailer = pdf.getTrailer();
doJSONObject(p, first_object, "trailer", trailer);
JSON::writeDictionaryKey(p, first_object, "trailer", 2);
pdf.getTrailer().writeJSON(1, p, true, 2);
first_object = false;
}
JSON::writeDictionaryClose(p, first_object, 1);
} else {
@ -3097,9 +3081,10 @@ QPDFJob::writeOutfile(QPDF& pdf)
try {
QUtil::remove_file(backup.c_str());
} catch (QPDFSystemError& e) {
*m->log->getError() << m->message_prefix << ": unable to delete original file ("
<< e.what() << ");" << " original file left in " << backup
<< ", but the input was successfully replaced\n";
*m->log->getError()
<< m->message_prefix << ": unable to delete original file (" << e.what() << ");"
<< " original file left in " << backup
<< ", but the input was successfully replaced\n";
}
}
}

View File

@ -3,6 +3,7 @@
#include <qpdf/BufferInputSource.hh>
#include <qpdf/Pl_Buffer.hh>
#include <qpdf/Pl_QPDFTokenizer.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QPDF.hh>
#include <qpdf/QPDFExc.hh>
#include <qpdf/QPDFLogger.hh>
@ -1617,10 +1618,33 @@ QPDFObjectHandle::getJSON(int json_version, bool dereference_indirect)
} else if (!dereference()) {
throw std::logic_error("attempted to dereference an uninitialized QPDFObjectHandle");
} else {
return obj->getJSON(json_version);
Pl_Buffer p{"json"};
JSON::Writer jw{&p, 0};
writeJSON(json_version, jw, dereference_indirect);
p.finish();
return JSON::parse(p.getString());
}
}
void
QPDFObjectHandle::writeJSON(int json_version, JSON::Writer& p, bool dereference_indirect)
{
if (!dereference_indirect && isIndirect()) {
p << "\"" << getObjGen().unparse(' ') << " R\"";
} else if (!dereference()) {
throw std::logic_error("attempted to dereference an uninitialized QPDFObjectHandle");
} else {
obj->writeJSON(json_version, p);
}
}
void
QPDFObjectHandle::writeJSON(int json_version, Pipeline* p, bool dereference_indirect, size_t depth)
{
JSON::Writer jw{p, depth};
writeJSON(json_version, jw, dereference_indirect);
}
JSON
QPDFObjectHandle::getStreamJSON(
int json_version,

View File

@ -1,5 +1,6 @@
#include <qpdf/QPDF_Array.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QPDFObjectHandle.hh>
#include <qpdf/QPDFObject_private.hh>
#include <qpdf/QTC.hh>
@ -148,36 +149,41 @@ QPDF_Array::unparse()
return result;
}
JSON
QPDF_Array::getJSON(int json_version)
void
QPDF_Array::writeJSON(int json_version, JSON::Writer& p)
{
static const JSON j_null = JSON::makeNull();
JSON j_array = JSON::makeArray();
p.writeStart('[');
if (sp) {
int next = 0;
for (auto& item: sp->elements) {
int key = item.first;
for (int j = next; j < key; ++j) {
j_array.addArrayElement(j_null);
p.writeNext() << "null";
}
p.writeNext();
auto og = item.second->getObjGen();
j_array.addArrayElement(
og.isIndirect() ? JSON::makeString(og.unparse(' ') + " R")
: item.second->getJSON(json_version));
if (og.isIndirect()) {
p << "\"" << og.unparse(' ') << " R\"";
} else {
item.second->writeJSON(json_version, p);
}
next = ++key;
}
for (int j = next; j < sp->size; ++j) {
j_array.addArrayElement(j_null);
p.writeNext() << "null";
}
} else {
for (auto const& item: elements) {
p.writeNext();
auto og = item->getObjGen();
j_array.addArrayElement(
og.isIndirect() ? JSON::makeString(og.unparse(' ') + " R")
: item->getJSON(json_version));
if (og.isIndirect()) {
p << "\"" << og.unparse(' ') << " R\"";
} else {
item->writeJSON(json_version, p);
}
}
}
return j_array;
p.writeEnd(']');
}
QPDFObjectHandle

View File

@ -1,5 +1,7 @@
#include <qpdf/QPDF_Bool.hh>
#include <qpdf/JSON_writer.hh>
QPDF_Bool::QPDF_Bool(bool val) :
QPDFValue(::ot_boolean, "boolean"),
val(val)
@ -24,10 +26,10 @@ QPDF_Bool::unparse()
return (val ? "true" : "false");
}
JSON
QPDF_Bool::getJSON(int json_version)
void
QPDF_Bool::writeJSON(int json_version, JSON::Writer& p)
{
return JSON::makeBool(this->val);
p << val;
}
bool

View File

@ -28,9 +28,8 @@ QPDF_Destroyed::unparse()
return "";
}
JSON
QPDF_Destroyed::getJSON(int json_version)
void
QPDF_Destroyed::writeJSON(int json_version, JSON::Writer& p)
{
throw std::logic_error("attempted to get JSON from a QPDFObjectHandle from a destroyed QPDF");
return JSON::makeNull();
}
}

View File

@ -1,5 +1,6 @@
#include <qpdf/QPDF_Dictionary.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QPDFObject_private.hh>
#include <qpdf/QPDF_Name.hh>
#include <qpdf/QPDF_Null.hh>
@ -67,28 +68,30 @@ QPDF_Dictionary::unparse()
return result;
}
JSON
QPDF_Dictionary::getJSON(int json_version)
void
QPDF_Dictionary::writeJSON(int json_version, JSON::Writer& p)
{
JSON j = JSON::makeDictionary();
p.writeStart('{');
for (auto& iter: this->items) {
if (!iter.second.isNull()) {
p.writeNext();
if (json_version == 1) {
j.addDictionaryMember(
QPDF_Name::normalizeName(iter.first), iter.second.getJSON(json_version));
p << "\"" << JSON::Writer::encode_string(QPDF_Name::normalizeName(iter.first))
<< "\": ";
} else if (auto res = QPDF_Name::analyzeJSONEncoding(iter.first); res.first) {
if (res.second) {
p << "\"" << iter.first << "\": ";
} else {
p << "\"" << JSON::Writer::encode_string(iter.first) << "\": ";
}
} else {
bool has_8bit_chars;
bool is_valid_utf8;
bool is_utf16;
QUtil::analyze_encoding(iter.first, has_8bit_chars, is_valid_utf8, is_utf16);
std::string key = !has_8bit_chars || is_valid_utf8
? iter.first
: "n:" + QPDF_Name::normalizeName(iter.first);
j.addDictionaryMember(key, iter.second.getJSON(json_version));
p << "\"n:" << JSON::Writer::encode_string(QPDF_Name::normalizeName(iter.first))
<< "\": ";
}
iter.second.writeJSON(json_version, p);
}
}
return j;
p.writeEnd('}');
}
bool

View File

@ -1,5 +1,7 @@
#include <qpdf/QPDF_InlineImage.hh>
#include <qpdf/JSON_writer.hh>
QPDF_InlineImage::QPDF_InlineImage(std::string const& val) :
QPDFValue(::ot_inlineimage, "inline-image"),
val(val)
@ -24,8 +26,8 @@ QPDF_InlineImage::unparse()
return this->val;
}
JSON
QPDF_InlineImage::getJSON(int json_version)
void
QPDF_InlineImage::writeJSON(int json_version, JSON::Writer& p)
{
return JSON::makeNull();
p << "null";
}

View File

@ -1,5 +1,6 @@
#include <qpdf/QPDF_Integer.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QUtil.hh>
QPDF_Integer::QPDF_Integer(long long val) :
@ -26,10 +27,10 @@ QPDF_Integer::unparse()
return std::to_string(this->val);
}
JSON
QPDF_Integer::getJSON(int json_version)
void
QPDF_Integer::writeJSON(int json_version, JSON::Writer& p)
{
return JSON::makeInt(this->val);
p << std::to_string(this->val);
}
long long

View File

@ -1,7 +1,10 @@
#include <qpdf/QPDF_Name.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QUtil.hh>
#include <string_view>
QPDF_Name::QPDF_Name(std::string const& name) :
QPDFValue(::ot_name, "name"),
name(name)
@ -51,20 +54,71 @@ QPDF_Name::unparse()
return normalizeName(this->name);
}
JSON
QPDF_Name::getJSON(int json_version)
std::pair<bool, bool>
QPDF_Name::analyzeJSONEncoding(const std::string& name)
{
if (json_version == 1) {
return JSON::makeString(normalizeName(this->name));
} else {
bool has_8bit_chars;
bool is_valid_utf8;
bool is_utf16;
QUtil::analyze_encoding(this->name, has_8bit_chars, is_valid_utf8, is_utf16);
if (!has_8bit_chars || is_valid_utf8) {
return JSON::makeString(this->name);
std::basic_string_view<unsigned char> view{
reinterpret_cast<const unsigned char*>(name.data()), name.size()};
int tail = 0; // Number of continuation characters expected.
bool tail2 = false; // Potential overlong 3 octet utf-8.
bool tail3 = false; // potential overlong 4 octet
bool needs_escaping = false;
for (auto const& c: view) {
if (tail) {
if ((c & 0xc0) != 0x80) {
return {false, false};
}
if (tail2) {
if ((c & 0xe0) == 0x80) {
return {false, false};
}
tail2 = false;
} else if (tail3) {
if ((c & 0xf0) == 0x80) {
return {false, false};
}
tail3 = false;
}
tail--;
} else if (c < 0x80) {
if (!needs_escaping) {
needs_escaping = !((c > 34 && c != '\\') || c == ' ' || c == 33);
}
} else if ((c & 0xe0) == 0xc0) {
if ((c & 0xfe) == 0xc0) {
return {false, false};
}
tail = 1;
} else if ((c & 0xf0) == 0xe0) {
tail2 = (c == 0xe0);
tail = 2;
} else if ((c & 0xf8) == 0xf0) {
tail3 = (c == 0xf0);
tail = 3;
} else {
return JSON::makeString("n:" + normalizeName(this->name));
return {false, false};
}
}
return {tail == 0, !needs_escaping};
}
void
QPDF_Name::writeJSON(int json_version, JSON::Writer& p)
{
// For performance reasons this code is duplicated in QPDF_Dictionary::writeJSON. When updating
// this method make sure QPDF_Dictionary is also update.
if (json_version == 1) {
p << "\"" << JSON::Writer::encode_string(normalizeName(name)) << "\"";
} else {
if (auto res = analyzeJSONEncoding(name); res.first) {
if (res.second) {
p << "\"" << name << "\"";
} else {
p << "\"" << JSON::Writer::encode_string(name) << "\"";
}
} else {
p << "\"n:" << JSON::Writer::encode_string(normalizeName(name)) << "\"";
}
}
}

View File

@ -1,5 +1,6 @@
#include <qpdf/QPDF_Null.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QPDFObject_private.hh>
QPDF_Null::QPDF_Null() :
@ -43,9 +44,8 @@ QPDF_Null::unparse()
return "null";
}
JSON
QPDF_Null::getJSON(int json_version)
void
QPDF_Null::writeJSON(int json_version, JSON::Writer& p)
{
// If this is updated, QPDF_Array::getJSON must also be updated.
return JSON::makeNull();
p << "null";
}

View File

@ -1,5 +1,7 @@
#include <qpdf/QPDF_Operator.hh>
#include <qpdf/JSON_writer.hh>
QPDF_Operator::QPDF_Operator(std::string const& val) :
QPDFValue(::ot_operator, "operator"),
val(val)
@ -24,8 +26,8 @@ QPDF_Operator::unparse()
return val;
}
JSON
QPDF_Operator::getJSON(int json_version)
void
QPDF_Operator::writeJSON(int json_version, JSON::Writer& p)
{
return JSON::makeNull();
p << "null";
}

View File

@ -1,5 +1,6 @@
#include <qpdf/QPDF_Real.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QUtil.hh>
QPDF_Real::QPDF_Real(std::string const& val) :
@ -38,21 +39,17 @@ QPDF_Real::unparse()
return this->val;
}
JSON
QPDF_Real::getJSON(int json_version)
void
QPDF_Real::writeJSON(int json_version, JSON::Writer& p)
{
// While PDF allows .x or -.x, JSON does not. Rather than converting from string to double and
// back, just handle this as a special case for JSON.
std::string result;
if (this->val.length() == 0) {
// Can't really happen...
result = "0";
p << "0";
} else if (this->val.at(0) == '.') {
result = "0" + this->val;
} else if ((this->val.length() >= 2) && (this->val.at(0) == '-') && (this->val.at(1) == '.')) {
result = "-0." + this->val.substr(2);
p << "0" << this->val;
} else if (this->val.length() >= 2 && this->val.at(0) == '-' && this->val.at(1) == '.') {
p << "-0." << this->val.substr(2);
} else {
result = this->val;
p << this->val;
}
return JSON::makeNumber(result);
}

View File

@ -26,9 +26,8 @@ QPDF_Reserved::unparse()
return "";
}
JSON
QPDF_Reserved::getJSON(int json_version)
void
QPDF_Reserved::writeJSON(int json_version, JSON::Writer& p)
{
throw std::logic_error("QPDFObjectHandle: attempting to get JSON from a reserved object");
return JSON::makeNull();
}

View File

@ -1,6 +1,7 @@
#include <qpdf/QPDF_Stream.hh>
#include <qpdf/ContentNormalizer.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/Pipeline.hh>
#include <qpdf/Pl_Base64.hh>
#include <qpdf/Pl_Buffer.hh>
@ -176,13 +177,10 @@ QPDF_Stream::unparse()
return og.unparse(' ') + " R";
}
JSON
QPDF_Stream::getJSON(int json_version)
void
QPDF_Stream::writeJSON(int json_version, JSON::Writer& jw)
{
if (json_version == 1) {
return this->stream_dict.getJSON(json_version);
}
return getStreamJSON(json_version, qpdf_sj_none, qpdf_dl_none, nullptr, "");
stream_dict.writeJSON(json_version, jw);
}
JSON
@ -192,78 +190,109 @@ QPDF_Stream::getStreamJSON(
qpdf_stream_decode_level_e decode_level,
Pipeline* p,
std::string const& data_filename)
{
Pl_Buffer pb{"streamjson"};
JSON::Writer jw{&pb, 0};
decode_level =
writeStreamJSON(json_version, jw, json_data, decode_level, p, data_filename, true);
pb.finish();
auto result = JSON::parse(pb.getString());
if (json_data == qpdf_sj_inline) {
result.addDictionaryMember("data", JSON::makeBlob(StreamBlobProvider(this, decode_level)));
}
return result;
}
qpdf_stream_decode_level_e
QPDF_Stream::writeStreamJSON(
int json_version,
JSON::Writer& jw,
qpdf_json_stream_data_e json_data,
qpdf_stream_decode_level_e decode_level,
Pipeline* p,
std::string const& data_filename,
bool no_data_key)
{
switch (json_data) {
case qpdf_sj_none:
case qpdf_sj_inline:
if (p != nullptr) {
throw std::logic_error("QPDF_Stream::getStreamJSON: pipeline should only be supplied "
throw std::logic_error("QPDF_Stream::writeStreamJSON: pipeline should only be supplied "
"when json_data is file");
}
break;
case qpdf_sj_file:
if (p == nullptr) {
throw std::logic_error(
"QPDF_Stream::getStreamJSON: pipeline must be supplied when json_data is file");
"QPDF_Stream::writeStreamJSON: pipeline must be supplied when json_data is file");
}
if (data_filename.empty()) {
throw std::logic_error("QPDF_Stream::getStreamJSON: data_filename must be supplied "
throw std::logic_error("QPDF_Stream::writeStreamJSON: data_filename must be supplied "
"when json_data is file");
}
break;
}
auto dict = this->stream_dict;
JSON result = JSON::makeDictionary();
if (json_data != qpdf_sj_none) {
Pl_Discard discard;
Pl_Buffer buf_pl{"stream data"};
// buf_pl contains valid data and is ready for retrieval of the data.
bool buf_pl_ready = false;
bool filtered = false;
bool filter = (decode_level != qpdf_dl_none);
for (int attempt = 1; attempt <= 2; ++attempt) {
Pipeline* data_pipeline = &discard;
if (json_data == qpdf_sj_file) {
// We need to capture the data to write
data_pipeline = &buf_pl;
}
bool succeeded =
pipeStreamData(data_pipeline, &filtered, 0, decode_level, false, (attempt == 1));
if (!succeeded || (filter && !filtered)) {
// Try again
filter = false;
decode_level = qpdf_dl_none;
buf_pl.getString(); // reset buf_pl
} else {
if (json_data == qpdf_sj_file) {
buf_pl_ready = true;
}
break;
}
}
// We can use unsafeShallowCopy because we are only touching top-level keys.
dict = this->stream_dict.unsafeShallowCopy();
dict.removeKey("/Length");
if (filter && filtered) {
dict.removeKey("/Filter");
dict.removeKey("/DecodeParms");
}
if (json_data == qpdf_sj_file) {
result.addDictionaryMember("datafile", JSON::makeString(data_filename));
if (!buf_pl_ready) {
throw std::logic_error("QPDF_Stream: failed to get stream data in json file mode");
}
p->writeString(buf_pl.getString());
} else if (json_data == qpdf_sj_inline) {
result.addDictionaryMember(
"data", JSON::makeBlob(StreamBlobProvider(this, decode_level)));
jw.writeStart('{');
if (json_data == qpdf_sj_none) {
jw.writeNext();
jw << R"("dict": )";
stream_dict.writeJSON(json_version, jw);
jw.writeEnd('}');
return decode_level;
}
Pl_Discard discard;
Pl_Buffer buf_pl{"stream data"};
Pipeline* data_pipeline = &buf_pl;
if (no_data_key && json_data == qpdf_sj_inline) {
data_pipeline = &discard;
}
// pipeStreamData produced valid data.
bool buf_pl_ready = false;
bool filtered = false;
bool filter = (decode_level != qpdf_dl_none);
for (int attempt = 1; attempt <= 2; ++attempt) {
bool succeeded =
pipeStreamData(data_pipeline, &filtered, 0, decode_level, false, (attempt == 1));
if (!succeeded || (filter && !filtered)) {
// Try again
filter = false;
decode_level = qpdf_dl_none;
buf_pl.getString(); // reset buf_pl
} else {
throw std::logic_error("QPDF_Stream: unexpected value of json_data");
buf_pl_ready = true;
break;
}
}
result.addDictionaryMember("dict", dict.getJSON(json_version));
return result;
if (!buf_pl_ready) {
throw std::logic_error("QPDF_Stream: failed to get stream data");
}
// We can use unsafeShallowCopy because we are only touching top-level keys.
auto dict = stream_dict.unsafeShallowCopy();
dict.removeKey("/Length");
if (filter && filtered) {
dict.removeKey("/Filter");
dict.removeKey("/DecodeParms");
}
if (json_data == qpdf_sj_file) {
jw.writeNext() << R"("datafile": ")" << JSON::Writer::encode_string(data_filename) << "\"";
p->writeString(buf_pl.getString());
} else if (json_data == qpdf_sj_inline) {
if (!no_data_key) {
jw.writeNext() << R"("data": ")";
jw.writeBase64(buf_pl.getString()) << "\"";
}
} else {
throw std::logic_error("QPDF_Stream::writeStreamJSON : unexpected value of json_data");
}
jw.writeNext() << R"("dict": )";
dict.writeJSON(json_version, jw);
jw.writeEnd('}');
return decode_level;
}
void

View File

@ -1,5 +1,6 @@
#include <qpdf/QPDF_String.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/QUtil.hh>
// DO NOT USE ctype -- it is locale dependent for some things, and it's not worth the risk of
@ -45,33 +46,28 @@ QPDF_String::unparse()
return unparse(false);
}
JSON
QPDF_String::getJSON(int json_version)
void
QPDF_String::writeJSON(int json_version, JSON::Writer& p)
{
auto candidate = getUTF8Val();
if (json_version == 1) {
return JSON::makeString(getUTF8Val());
}
// See if we can unambiguously represent as Unicode.
bool is_unicode = false;
std::string result;
std::string candidate = getUTF8Val();
if (QUtil::is_utf16(this->val) || QUtil::is_explicit_utf8(this->val)) {
is_unicode = true;
result = candidate;
} else if (!useHexString()) {
std::string test;
if (QUtil::utf8_to_pdf_doc(candidate, test, '?') && (test == this->val)) {
// This is a PDF-doc string that can be losslessly encoded as Unicode.
is_unicode = true;
result = candidate;
}
}
if (is_unicode) {
result = "u:" + result;
p << "\"" << JSON::Writer::encode_string(candidate) << "\"";
} else {
result = "b:" + QUtil::hex_encode(this->val);
// See if we can unambiguously represent as Unicode.
if (QUtil::is_utf16(this->val) || QUtil::is_explicit_utf8(this->val)) {
p << "\"u:" << JSON::Writer::encode_string(candidate) <<"\"";
return;
} else if (!useHexString()) {
std::string test;
if (QUtil::utf8_to_pdf_doc(candidate, test, '?') && (test == this->val)) {
// This is a PDF-doc string that can be losslessly encoded as Unicode.
p << "\"u:" << JSON::Writer::encode_string(candidate) <<"\"";
return;
}
}
p << "\"b:" << QUtil::hex_encode(val) <<"\"";
}
return JSON::makeString(result);
}
bool

View File

@ -27,9 +27,8 @@ QPDF_Unresolved::unparse()
return "";
}
JSON
QPDF_Unresolved::getJSON(int json_version)
void
QPDF_Unresolved::writeJSON(int json_version, JSON::Writer& p)
{
throw std::logic_error("attempted to get JSON from an unresolved QPDFObjectHandle");
return JSON::makeNull();
}

View File

@ -1,12 +1,14 @@
#include <qpdf/QPDF.hh>
#include <qpdf/FileInputSource.hh>
#include <qpdf/JSON_writer.hh>
#include <qpdf/Pl_Base64.hh>
#include <qpdf/Pl_StdioFile.hh>
#include <qpdf/QIntC.hh>
#include <qpdf/QPDFObject_private.hh>
#include <qpdf/QPDFValue.hh>
#include <qpdf/QPDF_Null.hh>
#include <qpdf/QPDF_Stream.hh>
#include <qpdf/QTC.hh>
#include <qpdf/QUtil.hh>
#include <algorithm>
@ -442,7 +444,9 @@ void
QPDF::JSONReactor::replaceObject(QPDFObjectHandle&& replacement, JSON const& value)
{
if (replacement.isIndirect()) {
error(replacement.getParsedOffset(), "the value of an object may not be an indirect object reference");
error(
replacement.getParsedOffset(),
"the value of an object may not be an indirect object reference");
return;
}
auto& tos = stack.back();
@ -828,45 +832,20 @@ QPDF::importJSON(std::shared_ptr<InputSource> is, bool must_be_complete)
}
void
QPDF::writeJSONStream(
writeJSONStreamFile(
int version,
Pipeline* p,
bool& first,
std::string const& key,
QPDFObjectHandle& obj,
JSON::Writer& jw,
QPDF_Stream& stream,
int id,
qpdf_stream_decode_level_e decode_level,
qpdf_json_stream_data_e json_stream_data,
std::string const& file_prefix)
{
Pipeline* stream_p = nullptr;
FILE* f = nullptr;
std::shared_ptr<Pl_StdioFile> f_pl;
std::string filename;
if (json_stream_data == qpdf_sj_file) {
filename = file_prefix + "-" + std::to_string(obj.getObjectID());
f = QUtil::safe_fopen(filename.c_str(), "wb");
f_pl = std::make_shared<Pl_StdioFile>("stream data", f);
stream_p = f_pl.get();
}
auto j = JSON::makeDictionary();
j.addDictionaryMember(
"stream", obj.getStreamJSON(version, json_stream_data, decode_level, stream_p, filename));
JSON::writeDictionaryItem(p, first, key, j, 3);
if (f) {
f_pl->finish();
f_pl = nullptr;
fclose(f);
}
}
void
QPDF::writeJSONObject(
int version, Pipeline* p, bool& first, std::string const& key, QPDFObjectHandle& obj)
{
auto j = JSON::makeDictionary();
j.addDictionaryMember("value", obj.getJSON(version, true));
JSON::writeDictionaryItem(p, first, key, j, 3);
auto filename = file_prefix + "-" + std::to_string(id);
auto* f = QUtil::safe_fopen(filename.c_str(), "wb");
Pl_StdioFile f_pl{"stream data", f};
stream.writeStreamJSON(version, jw, qpdf_sj_file, decode_level, &f_pl, filename);
f_pl.finish();
fclose(f);
}
void
@ -893,80 +872,75 @@ QPDF::writeJSON(
std::string const& file_prefix,
std::set<std::string> wanted_objects)
{
int const depth_outer = 1;
int const depth_top = 1;
int const depth_qpdf = 2;
int const depth_qpdf_inner = 3;
if (version != 2) {
throw std::runtime_error("QPDF::writeJSON: only version 2 is supported");
}
bool first = true;
JSON::Writer jw{p, 4};
if (complete) {
JSON::writeDictionaryOpen(p, first, depth_outer);
} else {
first = first_key;
jw << "{";
} else if (!first_key) {
jw << ",";
}
JSON::writeDictionaryKey(p, first, "qpdf", depth_top);
bool first_qpdf = true;
JSON::writeArrayOpen(p, first_qpdf, depth_top);
JSON::writeNext(p, first_qpdf, depth_qpdf);
bool first_qpdf_inner = true;
JSON::writeDictionaryOpen(p, first_qpdf_inner, depth_qpdf);
JSON::writeDictionaryItem(
p, first_qpdf_inner, "jsonversion", JSON::makeInt(version), depth_qpdf_inner);
JSON::writeDictionaryItem(
p, first_qpdf_inner, "pdfversion", JSON::makeString(getPDFVersion()), depth_qpdf_inner);
JSON::writeDictionaryItem(
p,
first_qpdf_inner,
"pushedinheritedpageresources",
JSON::makeBool(everPushedInheritedAttributesToPages()),
depth_qpdf_inner);
JSON::writeDictionaryItem(
p,
first_qpdf_inner,
"calledgetallpages",
JSON::makeBool(everCalledGetAllPages()),
depth_qpdf_inner);
JSON::writeDictionaryItem(
p,
first_qpdf_inner,
"maxobjectid",
JSON::makeInt(QIntC::to_longlong(getObjectCount())),
depth_qpdf_inner);
JSON::writeDictionaryClose(p, first_qpdf_inner, depth_qpdf);
JSON::writeNext(p, first_qpdf, depth_qpdf);
JSON::writeDictionaryOpen(p, first_qpdf_inner, depth_qpdf);
first_key = false;
/* clang-format off */
jw << "\n"
" \"qpdf\": [\n"
" {\n"
" \"jsonversion\": " << std::to_string(version) << ",\n"
" \"pdfversion\": \"" << getPDFVersion() << "\",\n"
" \"pushedinheritedpageresources\": " << (everPushedInheritedAttributesToPages() ? "true" : "false") << ",\n"
" \"calledgetallpages\": " << (everCalledGetAllPages() ? "true" : "false") << ",\n"
" \"maxobjectid\": " << std::to_string(getObjectCount()) << "\n"
" },\n"
" {";
/* clang-format on */
bool all_objects = wanted_objects.empty();
bool first = true;
for (auto& obj: getAllObjects()) {
std::string key = "obj:" + obj.unparse();
auto const og = obj.getObjGen();
std::string key = "obj:" + og.unparse(' ') + " R";
if (all_objects || wanted_objects.count(key)) {
if (obj.isStream()) {
writeJSONStream(
version,
p,
first_qpdf_inner,
key,
obj,
decode_level,
json_stream_data,
file_prefix);
if (first) {
jw << "\n \"" << key;
first = false;
} else {
writeJSONObject(version, p, first_qpdf_inner, key, obj);
jw << "\n },\n \"" << key;
}
if (auto* stream = obj.getObjectPtr()->as<QPDF_Stream>()) {
jw << "\": {\n \"stream\": ";
if (json_stream_data == qpdf_sj_file) {
writeJSONStreamFile(
version, jw, *stream, og.getObj(), decode_level, file_prefix);
} else {
stream->writeStreamJSON(
version, jw, json_stream_data, decode_level, nullptr, "");
}
} else {
jw << "\": {\n \"value\": ";
obj.writeJSON(version, jw, true);
}
}
}
if (all_objects || wanted_objects.count("trailer")) {
auto trailer = getTrailer();
writeJSONObject(version, p, first_qpdf_inner, "trailer", trailer);
if (!first) {
jw << "\n },";
}
jw << "\n \"trailer\": {\n \"value\": ";
getTrailer().writeJSON(version, jw, true);
first = false;
}
JSON::writeDictionaryClose(p, first_qpdf_inner, depth_qpdf);
JSON::writeArrayClose(p, first_qpdf, depth_top);
if (!first) {
jw << "\n }";
}
/* clang-format off */
jw << "\n"
" }\n"
" ]";
/* clang-format on */
if (complete) {
JSON::writeDictionaryClose(p, first, 0);
*p << "\n";
jw << "\n}\n";
p->finish();
}
first_key = false;
}

137
libqpdf/qpdf/JSON_writer.hh Normal file
View File

@ -0,0 +1,137 @@
#ifndef JSON_WRITER_HH
#define JSON_WRITER_HH
#include <qpdf/JSON.hh>
#include <qpdf/Pipeline.hh>
#include <qpdf/Pl_Base64.hh>
#include <qpdf/Pl_Concatenate.hh>
#include <string_view>
// Writer is a small utility class to aid writing JSON to a pipeline. Methods are designed to allow
// chaining of calls.
//
// Some uses of the class have a significant performance impact. The class is intended purely for
// internal use to allow it to be adapted as needed to maintain performance.
class JSON::Writer
{
public:
Writer(Pipeline* p, size_t depth) :
p(p),
indent(2 * depth)
{
}
Writer&
write(char const* data, size_t len)
{
p->write(reinterpret_cast<unsigned char const*>(data), len);
return *this;
}
Writer&
writeBase64(std::string_view sv)
{
Pl_Concatenate cat{"writer concat", p};
Pl_Base64 base{"writer base64", &cat, Pl_Base64::a_encode};
base.write(reinterpret_cast<unsigned char const*>(sv.data()), sv.size());
base.finish();
return *this;
}
Writer&
writeNext()
{
auto n = indent;
if (first) {
first = false;
write(&spaces[1], n % n_spaces + 1);
} else {
write(&spaces[0], n % n_spaces + 2);
}
while (n >= n_spaces) {
write(&spaces[2], n_spaces);
n -= n_spaces;
}
return *this;
}
Writer&
writeStart(char const& c)
{
write(&c, 1);
first = true;
indent += 2;
return *this;
}
Writer&
writeEnd(char const& c)
{
if (indent > 1) {
indent -= 2;
}
if (!first) {
first = true;
writeNext();
}
first = false;
write(&c, 1);
return *this;
}
Writer&
operator<<(std::string_view sv)
{
p->write(reinterpret_cast<unsigned char const*>(sv.data()), sv.size());
return *this;
}
Writer&
operator<<(char const* s)
{
*this << std::string_view{s};
return *this;
}
Writer&
operator<<(bool val)
{
*this << (val ? "true" : "false");
return *this;
}
Writer&
operator<<(int val)
{
*this << std::to_string(val);
return *this;
}
Writer&
operator<<(size_t val)
{
*this << std::to_string(val);
return *this;
}
Writer&
operator<<(JSON&& j)
{
j.write(p, indent / 2);
return *this;
}
static std::string encode_string(std::string const& utf8);
private:
Pipeline* p;
bool first{true};
size_t indent;
static constexpr std::string_view spaces =
",\n ";
static constexpr auto n_spaces = spaces.size() - 2;
};
#endif // JSON_WRITER_HH

View File

@ -33,10 +33,10 @@ class QPDFObject
{
return value->unparse();
}
JSON
getJSON(int json_version)
void
writeJSON(int json_version, JSON::Writer& p)
{
return value->getJSON(json_version);
return value->writeJSON(json_version, p);
}
std::string
getStringValue() const

View File

@ -24,7 +24,7 @@ class QPDFValue: public std::enable_shared_from_this<QPDFValue>
virtual std::shared_ptr<QPDFObject> copy(bool shallow = false) = 0;
virtual std::string unparse() = 0;
virtual JSON getJSON(int json_version) = 0;
virtual void writeJSON(int json_version, JSON::Writer& p) = 0;
struct JSON_Descr
{

View File

@ -22,7 +22,7 @@ class QPDF_Array: public QPDFValue
create(std::vector<std::shared_ptr<QPDFObject>>&& items, bool sparse);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
void disconnect() override;
int

View File

@ -10,7 +10,8 @@ class QPDF_Bool: public QPDFValue
static std::shared_ptr<QPDFObject> create(bool val);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
bool getVal() const;
private:

View File

@ -9,7 +9,7 @@ class QPDF_Destroyed: public QPDFValue
~QPDF_Destroyed() override = default;
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
static std::shared_ptr<QPDFValue> getInstance();
private:

View File

@ -16,7 +16,7 @@ class QPDF_Dictionary: public QPDFValue
static std::shared_ptr<QPDFObject> create(std::map<std::string, QPDFObjectHandle>&& items);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
void disconnect() override;
// hasKey() and getKeys() treat keys with null values as if they aren't there. getKey() returns

View File

@ -10,7 +10,7 @@ class QPDF_InlineImage: public QPDFValue
static std::shared_ptr<QPDFObject> create(std::string const& val);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
std::string
getStringValue() const override
{

View File

@ -10,7 +10,7 @@ class QPDF_Integer: public QPDFValue
static std::shared_ptr<QPDFObject> create(long long value);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
long long getVal() const;
private:

View File

@ -10,10 +10,15 @@ class QPDF_Name: public QPDFValue
static std::shared_ptr<QPDFObject> create(std::string const& name);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
// Put # into strings with characters unsuitable for name token
static std::string normalizeName(std::string const& name);
// Check whether name is valid utf-8 and whether it contains characters that require escaping.
// Return {false, false} if the name is not valid utf-8, otherwise return {true, true} if no
// characters require or {true, false} if escaping is required.
static std::pair<bool, bool> analyzeJSONEncoding(std::string const& name);
std::string
getStringValue() const override
{

View File

@ -18,7 +18,7 @@ class QPDF_Null: public QPDFValue
std::string var_descr);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
private:
QPDF_Null();

View File

@ -10,7 +10,7 @@ class QPDF_Operator: public QPDFValue
static std::shared_ptr<QPDFObject> create(std::string const& val);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
std::string
getStringValue() const override
{

View File

@ -12,7 +12,7 @@ class QPDF_Real: public QPDFValue
create(double value, int decimal_places, bool trim_trailing_zeroes);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
std::string
getStringValue() const override
{

View File

@ -10,7 +10,7 @@ class QPDF_Reserved: public QPDFValue
static std::shared_ptr<QPDFObject> create();
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
private:
QPDF_Reserved();

View File

@ -25,7 +25,7 @@ class QPDF_Stream: public QPDFValue
size_t length);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
void setDescription(
QPDF*, std::shared_ptr<QPDFValue::Description>& description, qpdf_offset_t offset) override;
void disconnect() override;
@ -64,6 +64,14 @@ class QPDF_Stream: public QPDFValue
qpdf_stream_decode_level_e decode_level,
Pipeline* p,
std::string const& data_filename);
qpdf_stream_decode_level_e writeStreamJSON(
int json_version,
JSON::Writer& jw,
qpdf_json_stream_data_e json_data,
qpdf_stream_decode_level_e decode_level,
Pipeline* p,
std::string const& data_filename,
bool no_data_key = false);
void replaceDict(QPDFObjectHandle const& new_dict);

View File

@ -16,7 +16,7 @@ class QPDF_String: public QPDFValue
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
std::string unparse(bool force_binary);
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
std::string getUTF8Val() const;
std::string
getStringValue() const override

View File

@ -10,7 +10,7 @@ class QPDF_Unresolved: public QPDFValue
static std::shared_ptr<QPDFObject> create(QPDF* qpdf, QPDFObjGen const& og);
std::shared_ptr<QPDFObject> copy(bool shallow = false) override;
std::string unparse() override;
JSON getJSON(int json_version) override;
void writeJSON(int json_version, JSON::Writer& p) override;
private:
QPDF_Unresolved(QPDF* qpdf, QPDFObjGen const& og);

View File

@ -33,5 +33,22 @@ $td->runtest("copy sparse array",
{$td->COMMAND => "test_driver 97 many-nulls.pdf"},
{$td->STRING => "test 97 done\n", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("copy file with many nulls",
{$td->COMMAND =>
"qpdf minimal-nulls.pdf --qdf --static-id --no-original-object-ids a.pdf"},
{$td->STRING => "", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("compare files",
{$td->FILE => "a.pdf"},
{$td->FILE => "minimal-nulls.pdf"});
$td->runtest("file with many nulls to JSON v1",
{$td->COMMAND => "qpdf minimal-nulls.pdf --json=1 -"},
{$td->FILE => "minimal-nulls-1.json", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("file with many nulls to JSON v2",
{$td->COMMAND => "qpdf minimal-nulls.pdf --json=2 -"},
{$td->FILE => "minimal-nulls-2.json", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
cleanup();
$td->report(4);
$td->report(8);

View File

@ -350,7 +350,7 @@ $td->runtest("check C API write to JSON stream",
# (using #xx) would generate invalid JSON, even though qpdf's own JSON
# parser would accept it. Also, the JSON spec allows real numbers in
# scientific notation, but the PDF spec does not.
$n_tests += 4;
$n_tests += 7;
$td->runtest("handle binary names",
{$td->COMMAND =>
"qpdf --json-output weird-tokens.pdf a.json"},
@ -371,6 +371,17 @@ $td->runtest("weird tokens with scientific notation",
"qpdf --json-input --json-output weird-tokens-alt.json -"},
{$td->FILE => "weird-tokens.json", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("handle binary names (JSON v1)",
{$td->COMMAND =>
"qpdf --json=1 weird-tokens.pdf a.json"},
{$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check json",
{$td->FILE => "a.json"},
{$td->FILE => "weird-tokens-v1.json"},
$td->NORMALIZE_NEWLINES);
$td->runtest("write JSON to pipeline",
{$td->COMMAND => "test_driver 98 minimal.pdf ''"},
{$td->STRING => "test 98 done\n", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
cleanup();
$td->report($n_tests);

View File

@ -0,0 +1,453 @@
{
"version": 1,
"parameters": {
"decodelevel": "generalized"
},
"pages": [
{
"contents": [
"4 0 R"
],
"images": [],
"label": null,
"object": "3 0 R",
"outlines": [],
"pageposfrom1": 1
}
],
"pagelabels": [],
"acroform": {
"fields": [],
"hasacroform": false,
"needappearances": false
},
"attachments": {},
"encrypt": {
"capabilities": {
"accessibility": true,
"extract": true,
"moddifyannotations": true,
"modify": true,
"modifyassembly": true,
"modifyforms": true,
"modifyother": true,
"printhigh": true,
"printlow": true
},
"encrypted": false,
"ownerpasswordmatched": false,
"parameters": {
"P": 0,
"R": 0,
"V": 0,
"bits": 0,
"filemethod": "none",
"key": null,
"method": "none",
"streammethod": "none",
"stringmethod": "none"
},
"recovereduserpassword": null,
"userpasswordmatched": false
},
"outlines": [],
"objects": {
"1 0 R": {
"/Pages": "2 0 R",
"/Type": "/Catalog"
},
"2 0 R": {
"/Count": 1,
"/Kids": [
"3 0 R"
],
"/Type": "/Pages"
},
"3 0 R": {
"/Contents": "4 0 R",
"/MediaBox": [
0,
0,
612,
792
],
"/Nulls": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"6 0 R",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
],
"/Parent": "2 0 R",
"/Resources": {
"/Font": {
"/F1": "7 0 R"
},
"/ProcSet": "8 0 R"
},
"/Type": "/Page"
},
"4 0 R": {
"/Length": "5 0 R"
},
"5 0 R": 44,
"6 0 R": null,
"7 0 R": {
"/BaseFont": "/Helvetica",
"/Encoding": "/WinAnsiEncoding",
"/Name": "/F1",
"/Subtype": "/Type1",
"/Type": "/Font"
},
"8 0 R": [
"/PDF",
"/Text"
],
"trailer": {
"/ID": [
"ÏîgE<67>EMÛÊߢ$²\u0005#",
"1AY&SXŠfi#—bd3…'Ł"
],
"/Root": "1 0 R",
"/Size": 9
}
},
"objectinfo": {
"1 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"2 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"3 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"4 0 R": {
"stream": {
"filter": null,
"is": true,
"length": 44
}
},
"5 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"6 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"7 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"8 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
}
}
}

View File

@ -0,0 +1,424 @@
{
"version": 2,
"parameters": {
"decodelevel": "generalized"
},
"pages": [
{
"contents": [
"4 0 R"
],
"images": [],
"label": null,
"object": "3 0 R",
"outlines": [],
"pageposfrom1": 1
}
],
"pagelabels": [],
"acroform": {
"fields": [],
"hasacroform": false,
"needappearances": false
},
"attachments": {},
"encrypt": {
"capabilities": {
"accessibility": true,
"extract": true,
"modify": true,
"modifyannotations": true,
"modifyassembly": true,
"modifyforms": true,
"modifyother": true,
"printhigh": true,
"printlow": true
},
"encrypted": false,
"ownerpasswordmatched": false,
"parameters": {
"P": 0,
"R": 0,
"V": 0,
"bits": 0,
"filemethod": "none",
"key": null,
"method": "none",
"streammethod": "none",
"stringmethod": "none"
},
"recovereduserpassword": null,
"userpasswordmatched": false
},
"outlines": [],
"qpdf": [
{
"jsonversion": 2,
"pdfversion": "1.3",
"pushedinheritedpageresources": false,
"calledgetallpages": true,
"maxobjectid": 8
},
{
"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
],
"/Nulls": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"6 0 R",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
10,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
],
"/Parent": "2 0 R",
"/Resources": {
"/Font": {
"/F1": "7 0 R"
},
"/ProcSet": "8 0 R"
},
"/Type": "/Page"
}
},
"obj:4 0 R": {
"stream": {
"dict": {
"/Length": "5 0 R"
}
}
},
"obj:5 0 R": {
"value": 44
},
"obj:6 0 R": {
"value": null
},
"obj:7 0 R": {
"value": {
"/BaseFont": "/Helvetica",
"/Encoding": "/WinAnsiEncoding",
"/Name": "/F1",
"/Subtype": "/Type1",
"/Type": "/Font"
}
},
"obj:8 0 R": {
"value": [
"/PDF",
"/Text"
]
},
"trailer": {
"value": {
"/ID": [
"b:cfee6745ad454ddb88cadfa224b20523",
"b:31415926535897932384626433832795"
],
"/Root": "1 0 R",
"/Size": 9
}
}
}
]
}

View File

@ -0,0 +1,387 @@
%PDF-1.3
%¿÷¢þ
%QDF-1.0
1 0 obj
<<
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [
3 0 R
]
/Type /Pages
>>
endobj
%% Page 1
3 0 obj
<<
/Contents 4 0 R
/MediaBox [
0
0
612
792
]
/Nulls [
null
null
null
null
null
null
null
null
null
null
6 0 R
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
10
null
null
null
null
null
null
null
null
null
null
]
/Parent 2 0 R
/Resources <<
/Font <<
/F1 7 0 R
>>
/ProcSet 8 0 R
>>
/Type /Page
>>
endobj
%% Contents for page 1
4 0 obj
<<
/Length 5 0 R
>>
stream
BT
/F1 24 Tf
72 720 Td
(Potato) Tj
ET
endstream
endobj
5 0 obj
44
endobj
6 0 obj
null
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
[
/PDF
/Text
]
endobj
xref
0 9
0000000000 65535 f
0000000025 00000 n
0000000079 00000 n
0000000161 00000 n
0000002909 00000 n
0000003008 00000 n
0000003027 00000 n
0000003048 00000 n
0000003166 00000 n
trailer <<
/Root 1 0 R
/Size 9
/ID [<cfee6745ad454ddb88cadfa224b20523><31415926535897932384626433832795>]
>>
startxref
3201
%%EOF

View File

@ -10,21 +10,155 @@
{
"obj:1 0 R": {
"value": {
"/Escape\\Key": 42,
"/Extra": [
"u:Names with binary data",
"n:/ABCDEF+#ba#da#cc#e5",
"n:/OVERLONG+#c0#81",
"n:/OVERLONG+#c1#ff",
"/Ok+€",
"n:/OVERLONG+#e0#81#82",
"n:/OVERLONG+#e0#9f#ff",
"/Ok+ࠀ",
"n:/OVERLONG+#f0#81#82#83",
"n:/OVERLONG+#f0#8f#ff#ff",
"/Ok+𐀀",
"n:/range+#01",
"n:/low+#18",
"/ABCEDEF+π",
"n:/one+#a0two",
"n:/text#2fplain",
"u:Names requiring escaping in JSON",
"/Back\\shlash",
"/Low\u0022",
"/Low\u001f",
"/ExceptSpace ",
"/Except!",
"u:Very small/large reals",
1e-05,
1e12
],
"/Nested": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": 42
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"/Pages": "2 0 R",
"/Type": "/Catalog",
"n:/WeirdKey+#ba#da#cc#e5": 42
@ -78,7 +212,7 @@
"value": {
"/ID": [
"b:42841c13bbf709d79a200fa1691836f8",
"b:728c020f464c3cf7e02c12605fa7d88b"
"b:31415926535897932384626433832795"
],
"/Root": "1 0 R",
"/Size": 7

View File

@ -0,0 +1,295 @@
{
"version": 1,
"parameters": {
"decodelevel": "generalized"
},
"pages": [
{
"contents": [
"4 0 R"
],
"images": [],
"label": null,
"object": "3 0 R",
"outlines": [],
"pageposfrom1": 1
}
],
"pagelabels": [],
"acroform": {
"fields": [],
"hasacroform": false,
"needappearances": false
},
"attachments": {},
"encrypt": {
"capabilities": {
"accessibility": true,
"extract": true,
"moddifyannotations": true,
"modify": true,
"modifyassembly": true,
"modifyforms": true,
"modifyother": true,
"printhigh": true,
"printlow": true
},
"encrypted": false,
"ownerpasswordmatched": false,
"parameters": {
"P": 0,
"R": 0,
"V": 0,
"bits": 0,
"filemethod": "none",
"key": null,
"method": "none",
"streammethod": "none",
"stringmethod": "none"
},
"recovereduserpassword": null,
"userpasswordmatched": false
},
"outlines": [],
"objects": {
"1 0 R": {
"/Escape\\Key": 42,
"/Extra": [
"Names with binary data",
"/ABCDEF+#ba#da#cc#e5",
"/OVERLONG+#c0#81",
"/OVERLONG+#c1#ff",
"/Ok+#c2#80",
"/OVERLONG+#e0#81#82",
"/OVERLONG+#e0#9f#ff",
"/Ok+#e0#a0#80",
"/OVERLONG+#f0#81#82#83",
"/OVERLONG+#f0#8f#ff#ff",
"/Ok+#f0#90#80#80",
"/range+#01",
"/low+#18",
"/ABCEDEF+#cf#80",
"/one+#a0two",
"/text#2fplain",
"Names requiring escaping in JSON",
"/Back\\shlash",
"/Low\"",
"/Low#1f",
"/ExceptSpace#20",
"/Except!",
"Very small/large reals",
0.00001,
1000000000000
],
"/Nested": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": 42
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"/Pages": "2 0 R",
"/Type": "/Catalog",
"/WeirdKey+#ba#da#cc#e5": 42
},
"2 0 R": {
"/Count": 1,
"/Kids": [
"3 0 R"
],
"/Type": "/Pages"
},
"3 0 R": {
"/Contents": "4 0 R",
"/MediaBox": [
0,
0,
612,
792
],
"/Parent": "2 0 R",
"/Resources": {
"/Font": {
"/F1": "6 0 R"
}
},
"/Type": "/Page"
},
"4 0 R": {
"/Length": "5 0 R"
},
"5 0 R": 44,
"6 0 R": {
"/BaseFont": "/Helvetica",
"/Encoding": "/WinAnsiEncoding",
"/Subtype": "/Type1",
"/Type": "/Font"
},
"trailer": {
"/ID": [
"B—˝\u0013»÷\t×ı \u000f¡i˘6ø",
"1AY&SXŠfi#—bd3…'Ł"
],
"/Root": "1 0 R",
"/Size": 7
}
},
"objectinfo": {
"1 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"2 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"3 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"4 0 R": {
"stream": {
"filter": null,
"is": true,
"length": 44
}
},
"5 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
},
"6 0 R": {
"stream": {
"filter": null,
"is": false,
"length": null
}
}
}
}

View File

@ -10,21 +10,155 @@
{
"obj:1 0 R": {
"value": {
"/Escape\\Key": 42,
"/Extra": [
"u:Names with binary data",
"n:/ABCDEF+#ba#da#cc#e5",
"n:/OVERLONG+#c0#81",
"n:/OVERLONG+#c1#ff",
"/Ok+€",
"n:/OVERLONG+#e0#81#82",
"n:/OVERLONG+#e0#9f#ff",
"/Ok+ࠀ",
"n:/OVERLONG+#f0#81#82#83",
"n:/OVERLONG+#f0#8f#ff#ff",
"/Ok+𐀀",
"/range+\u0001",
"/low+\u0018",
"/ABCEDEF+π",
"n:/one+#a0two",
"/text/plain",
"u:Names requiring escaping in JSON",
"/Back\\shlash",
"/Low\"",
"/Low\u001f",
"/ExceptSpace ",
"/Except!",
"u:Very small/large reals",
0.00001,
1000000000000
],
"/Nested": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": {
"/1": {
"/2": {
"/3": {
"/4": {
"/5": {
"/6": {
"/7": {
"/8": {
"/9": {
"/10": 42
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"/Pages": "2 0 R",
"/Type": "/Catalog",
"n:/WeirdKey+#ba#da#cc#e5": 42
@ -78,7 +212,7 @@
"value": {
"/ID": [
"b:42841c13bbf709d79a200fa1691836f8",
"b:728c020f464c3cf7e02c12605fa7d88b"
"b:31415926535897932384626433832795"
],
"/Root": "1 0 R",
"/Size": 7

View File

@ -4,21 +4,155 @@
1 0 obj
<<
/Escape\Key 42
/Extra [
(Names with binary data)
/ABCDEF+#ba#da#cc#e5
/OVERLONG+#c0#81
/OVERLONG+#c1#ff
/Ok+#c2#80
/OVERLONG+#e0#81#82
/OVERLONG+#e0#9f#ff
/Ok+#e0#a0#80
/OVERLONG+#f0#81#82#83
/OVERLONG+#f0#8f#ff#ff
/Ok+#f0#90#80#80
/range+#01
/low+#18
/ABCEDEF+#cf#80
/one+#a0two
/text#2fplain
(Names requiring escaping in JSON)
/Back\shlash
/Low"
/Low#1f
/ExceptSpace#20
/Except!
(Very small/large reals)
0.00001
1000000000000
]
/Nested <<
/1 <<
/2 <<
/3 <<
/4 <<
/5 <<
/6 <<
/7 <<
/8 <<
/9 <<
/10 <<
/1 <<
/2 <<
/3 <<
/4 <<
/5 <<
/6 <<
/7 <<
/8 <<
/9 <<
/10 <<
/1 <<
/2 <<
/3 <<
/4 <<
/5 <<
/6 <<
/7 <<
/8 <<
/9 <<
/10 <<
/1 <<
/2 <<
/3 <<
/4 <<
/5 <<
/6 <<
/7 <<
/8 <<
/9 <<
/10 <<
/1 <<
/2 <<
/3 <<
/4 <<
/5 <<
/6 <<
/7 <<
/8 <<
/9 <<
/10 <<
/1 <<
/2 <<
/3 <<
/4 <<
/5 <<
/6 <<
/7 <<
/8 <<
/9 <<
/10 42
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
>>
/Pages 2 0 R
/Type /Catalog
/WeirdKey+#ba#da#cc#e5 42
@ -86,16 +220,16 @@ xref
0 7
0000000000 65535 f
0000000025 00000 n
0000000389 00000 n
0000000471 00000 n
0000000667 00000 n
0000000766 00000 n
0000000785 00000 n
0000008642 00000 n
0000008724 00000 n
0000008920 00000 n
0000009019 00000 n
0000009038 00000 n
trailer <<
/Root 1 0 R
/Size 7
/ID [<42841c13bbf709d79a200fa1691836f8><728c020f464c3cf7e02c12605fa7d88b>]
/ID [<42841c13bbf709d79a200fa1691836f8><31415926535897932384626433832795>]
>>
startxref
891
9144
%%EOF

View File

@ -3382,6 +3382,22 @@ test_97(QPDF& pdf, char const* arg2)
assert(nulls.unparse() == nulls2.unparse());
}
static void
test_98(QPDF& pdf, char const* arg2)
{
// Test QPDFObjectHandle::writeJSON. This test is built for minimal.pdf.
for (int i = 1; i < 7; ++i) {
auto oh = pdf.getObject(i, 0);
Pl_Buffer bf1{"write", nullptr};
Pl_Buffer bf2{"get", nullptr};
oh.writeJSON(JSON::LATEST, &bf1, true, 7);
bf1.finish();
oh.getJSON(JSON::LATEST, true).write(&bf2, 7);
bf2.finish();
assert(bf1.getString() == bf2.getString());
}
}
void
runtest(int n, char const* filename1, char const* arg2)
{
@ -3483,7 +3499,7 @@ runtest(int n, char const* filename1, char const* arg2)
{78, test_78}, {79, test_79}, {80, test_80}, {81, test_81}, {82, test_82}, {83, test_83},
{84, test_84}, {85, test_85}, {86, test_86}, {87, test_87}, {88, test_88}, {89, test_89},
{90, test_90}, {91, test_91}, {92, test_92}, {93, test_93}, {94, test_94}, {95, test_95},
{96, test_96}, {97, test_97}};
{96, test_96}, {97, test_97}, {98, test_98}};
auto fn = test_functions.find(n);
if (fn == test_functions.end()) {