diff --git a/include/qpdf/JSON.hh b/include/qpdf/JSON.hh index e3c8a7dc..3272800d 100644 --- a/include/qpdf/JSON.hh +++ b/include/qpdf/JSON.hh @@ -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 { diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index a836c3c9..04b11cba 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -1411,19 +1411,6 @@ class QPDF // JSON import void importJSON(std::shared_ptr, 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 static qpdf_offset_t diff --git a/include/qpdf/QPDFJob.hh b/include/qpdf/QPDFJob.hh index c291d1e8..b26a9dcf 100644 --- a/include/qpdf/QPDFJob.hh +++ b/include/qpdf/QPDFJob.hh @@ -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); diff --git a/include/qpdf/QPDFObjectHandle.hh b/include/qpdf/QPDFObjectHandle.hh index b2835495..9ea329ff 100644 --- a/include/qpdf/QPDFObjectHandle.hh +++ b/include/qpdf/QPDFObjectHandle.hh @@ -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(); diff --git a/libqpdf/JSON.cc b/libqpdf/JSON.cc index 27405df7..bc28f236 100644 --- a/libqpdf/JSON.cc +++ b/libqpdf/JSON.cc @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -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(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"); } diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index a699e38d..a85ff0a2 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -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"; } } } diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index d543f98e..9b9849a3 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -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, diff --git a/libqpdf/QPDF_Array.cc b/libqpdf/QPDF_Array.cc index 789acc35..ab2a4c8c 100644 --- a/libqpdf/QPDF_Array.cc +++ b/libqpdf/QPDF_Array.cc @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -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 diff --git a/libqpdf/QPDF_Bool.cc b/libqpdf/QPDF_Bool.cc index 05c52f22..97f47881 100644 --- a/libqpdf/QPDF_Bool.cc +++ b/libqpdf/QPDF_Bool.cc @@ -1,5 +1,7 @@ #include +#include + 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 diff --git a/libqpdf/QPDF_Destroyed.cc b/libqpdf/QPDF_Destroyed.cc index 4e34b508..5e04566a 100644 --- a/libqpdf/QPDF_Destroyed.cc +++ b/libqpdf/QPDF_Destroyed.cc @@ -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(); -} +} \ No newline at end of file diff --git a/libqpdf/QPDF_Dictionary.cc b/libqpdf/QPDF_Dictionary.cc index f7e32fc9..9332b1d3 100644 --- a/libqpdf/QPDF_Dictionary.cc +++ b/libqpdf/QPDF_Dictionary.cc @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -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 diff --git a/libqpdf/QPDF_InlineImage.cc b/libqpdf/QPDF_InlineImage.cc index 18f2fed6..2d62071d 100644 --- a/libqpdf/QPDF_InlineImage.cc +++ b/libqpdf/QPDF_InlineImage.cc @@ -1,5 +1,7 @@ #include +#include + 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"; } diff --git a/libqpdf/QPDF_Integer.cc b/libqpdf/QPDF_Integer.cc index 716a11e0..aa0437a1 100644 --- a/libqpdf/QPDF_Integer.cc +++ b/libqpdf/QPDF_Integer.cc @@ -1,5 +1,6 @@ #include +#include #include 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 diff --git a/libqpdf/QPDF_Name.cc b/libqpdf/QPDF_Name.cc index 5fde9c65..0e7f441a 100644 --- a/libqpdf/QPDF_Name.cc +++ b/libqpdf/QPDF_Name.cc @@ -1,7 +1,10 @@ #include +#include #include +#include + 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 +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 view{ + reinterpret_cast(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)) << "\""; } } } diff --git a/libqpdf/QPDF_Null.cc b/libqpdf/QPDF_Null.cc index fdabdfa7..fa4b6cab 100644 --- a/libqpdf/QPDF_Null.cc +++ b/libqpdf/QPDF_Null.cc @@ -1,5 +1,6 @@ #include +#include #include 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"; } diff --git a/libqpdf/QPDF_Operator.cc b/libqpdf/QPDF_Operator.cc index d1b2969d..c35a8391 100644 --- a/libqpdf/QPDF_Operator.cc +++ b/libqpdf/QPDF_Operator.cc @@ -1,5 +1,7 @@ #include +#include + 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"; } diff --git a/libqpdf/QPDF_Real.cc b/libqpdf/QPDF_Real.cc index 1d954dcd..f4304397 100644 --- a/libqpdf/QPDF_Real.cc +++ b/libqpdf/QPDF_Real.cc @@ -1,5 +1,6 @@ #include +#include #include 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); } diff --git a/libqpdf/QPDF_Reserved.cc b/libqpdf/QPDF_Reserved.cc index 845d6ebc..da112acc 100644 --- a/libqpdf/QPDF_Reserved.cc +++ b/libqpdf/QPDF_Reserved.cc @@ -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(); } diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index a43d91ff..6cfcdd46 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -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 diff --git a/libqpdf/QPDF_String.cc b/libqpdf/QPDF_String.cc index 3886b399..f425b313 100644 --- a/libqpdf/QPDF_String.cc +++ b/libqpdf/QPDF_String.cc @@ -1,5 +1,6 @@ #include +#include #include // 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 diff --git a/libqpdf/QPDF_Unresolved.cc b/libqpdf/QPDF_Unresolved.cc index fbf5e15f..dc14c2f2 100644 --- a/libqpdf/QPDF_Unresolved.cc +++ b/libqpdf/QPDF_Unresolved.cc @@ -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(); } diff --git a/libqpdf/QPDF_json.cc b/libqpdf/QPDF_json.cc index 8326e6a5..b06b70c9 100644 --- a/libqpdf/QPDF_json.cc +++ b/libqpdf/QPDF_json.cc @@ -1,12 +1,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -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 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 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("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 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()) { + 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; } diff --git a/libqpdf/qpdf/JSON_writer.hh b/libqpdf/qpdf/JSON_writer.hh new file mode 100644 index 00000000..3f770c58 --- /dev/null +++ b/libqpdf/qpdf/JSON_writer.hh @@ -0,0 +1,137 @@ +#ifndef JSON_WRITER_HH +#define JSON_WRITER_HH + +#include +#include +#include +#include + +#include + +// 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(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(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(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 diff --git a/libqpdf/qpdf/QPDFObject_private.hh b/libqpdf/qpdf/QPDFObject_private.hh index 5e87c215..1c3dadc9 100644 --- a/libqpdf/qpdf/QPDFObject_private.hh +++ b/libqpdf/qpdf/QPDFObject_private.hh @@ -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 diff --git a/libqpdf/qpdf/QPDFValue.hh b/libqpdf/qpdf/QPDFValue.hh index db8fb923..28abf147 100644 --- a/libqpdf/qpdf/QPDFValue.hh +++ b/libqpdf/qpdf/QPDFValue.hh @@ -24,7 +24,7 @@ class QPDFValue: public std::enable_shared_from_this virtual std::shared_ptr 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 { diff --git a/libqpdf/qpdf/QPDF_Array.hh b/libqpdf/qpdf/QPDF_Array.hh index 281e1d48..88b2a3d3 100644 --- a/libqpdf/qpdf/QPDF_Array.hh +++ b/libqpdf/qpdf/QPDF_Array.hh @@ -22,7 +22,7 @@ class QPDF_Array: public QPDFValue create(std::vector>&& items, bool sparse); std::shared_ptr 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 diff --git a/libqpdf/qpdf/QPDF_Bool.hh b/libqpdf/qpdf/QPDF_Bool.hh index 0b4a71fd..1692bdc4 100644 --- a/libqpdf/qpdf/QPDF_Bool.hh +++ b/libqpdf/qpdf/QPDF_Bool.hh @@ -10,7 +10,8 @@ class QPDF_Bool: public QPDFValue static std::shared_ptr create(bool val); std::shared_ptr 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: diff --git a/libqpdf/qpdf/QPDF_Destroyed.hh b/libqpdf/qpdf/QPDF_Destroyed.hh index 72e9130a..9259a2dc 100644 --- a/libqpdf/qpdf/QPDF_Destroyed.hh +++ b/libqpdf/qpdf/QPDF_Destroyed.hh @@ -9,7 +9,7 @@ class QPDF_Destroyed: public QPDFValue ~QPDF_Destroyed() override = default; std::shared_ptr 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 getInstance(); private: diff --git a/libqpdf/qpdf/QPDF_Dictionary.hh b/libqpdf/qpdf/QPDF_Dictionary.hh index 0fb6636e..8713a450 100644 --- a/libqpdf/qpdf/QPDF_Dictionary.hh +++ b/libqpdf/qpdf/QPDF_Dictionary.hh @@ -16,7 +16,7 @@ class QPDF_Dictionary: public QPDFValue static std::shared_ptr create(std::map&& items); std::shared_ptr 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 diff --git a/libqpdf/qpdf/QPDF_InlineImage.hh b/libqpdf/qpdf/QPDF_InlineImage.hh index bee12354..c06662d7 100644 --- a/libqpdf/qpdf/QPDF_InlineImage.hh +++ b/libqpdf/qpdf/QPDF_InlineImage.hh @@ -10,7 +10,7 @@ class QPDF_InlineImage: public QPDFValue static std::shared_ptr create(std::string const& val); std::shared_ptr 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 { diff --git a/libqpdf/qpdf/QPDF_Integer.hh b/libqpdf/qpdf/QPDF_Integer.hh index 2c2cf2f9..ae7f7893 100644 --- a/libqpdf/qpdf/QPDF_Integer.hh +++ b/libqpdf/qpdf/QPDF_Integer.hh @@ -10,7 +10,7 @@ class QPDF_Integer: public QPDFValue static std::shared_ptr create(long long value); std::shared_ptr 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: diff --git a/libqpdf/qpdf/QPDF_Name.hh b/libqpdf/qpdf/QPDF_Name.hh index c14d8659..b5d3c318 100644 --- a/libqpdf/qpdf/QPDF_Name.hh +++ b/libqpdf/qpdf/QPDF_Name.hh @@ -10,10 +10,15 @@ class QPDF_Name: public QPDFValue static std::shared_ptr create(std::string const& name); std::shared_ptr 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 analyzeJSONEncoding(std::string const& name); std::string getStringValue() const override { diff --git a/libqpdf/qpdf/QPDF_Null.hh b/libqpdf/qpdf/QPDF_Null.hh index a59b7509..fc6e0b5f 100644 --- a/libqpdf/qpdf/QPDF_Null.hh +++ b/libqpdf/qpdf/QPDF_Null.hh @@ -18,7 +18,7 @@ class QPDF_Null: public QPDFValue std::string var_descr); std::shared_ptr 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(); diff --git a/libqpdf/qpdf/QPDF_Operator.hh b/libqpdf/qpdf/QPDF_Operator.hh index 77aa5a17..b9b040d6 100644 --- a/libqpdf/qpdf/QPDF_Operator.hh +++ b/libqpdf/qpdf/QPDF_Operator.hh @@ -10,7 +10,7 @@ class QPDF_Operator: public QPDFValue static std::shared_ptr create(std::string const& val); std::shared_ptr 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 { diff --git a/libqpdf/qpdf/QPDF_Real.hh b/libqpdf/qpdf/QPDF_Real.hh index db5e0940..aa9baa5f 100644 --- a/libqpdf/qpdf/QPDF_Real.hh +++ b/libqpdf/qpdf/QPDF_Real.hh @@ -12,7 +12,7 @@ class QPDF_Real: public QPDFValue create(double value, int decimal_places, bool trim_trailing_zeroes); std::shared_ptr 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 { diff --git a/libqpdf/qpdf/QPDF_Reserved.hh b/libqpdf/qpdf/QPDF_Reserved.hh index 9ba855b7..801987e4 100644 --- a/libqpdf/qpdf/QPDF_Reserved.hh +++ b/libqpdf/qpdf/QPDF_Reserved.hh @@ -10,7 +10,7 @@ class QPDF_Reserved: public QPDFValue static std::shared_ptr create(); std::shared_ptr 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(); diff --git a/libqpdf/qpdf/QPDF_Stream.hh b/libqpdf/qpdf/QPDF_Stream.hh index 8488e157..3fcbc428 100644 --- a/libqpdf/qpdf/QPDF_Stream.hh +++ b/libqpdf/qpdf/QPDF_Stream.hh @@ -25,7 +25,7 @@ class QPDF_Stream: public QPDFValue size_t length); std::shared_ptr 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& 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); diff --git a/libqpdf/qpdf/QPDF_String.hh b/libqpdf/qpdf/QPDF_String.hh index c34cafef..967b2d30 100644 --- a/libqpdf/qpdf/QPDF_String.hh +++ b/libqpdf/qpdf/QPDF_String.hh @@ -16,7 +16,7 @@ class QPDF_String: public QPDFValue std::shared_ptr 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 diff --git a/libqpdf/qpdf/QPDF_Unresolved.hh b/libqpdf/qpdf/QPDF_Unresolved.hh index 0a1fa9a5..b4c1d9e4 100644 --- a/libqpdf/qpdf/QPDF_Unresolved.hh +++ b/libqpdf/qpdf/QPDF_Unresolved.hh @@ -10,7 +10,7 @@ class QPDF_Unresolved: public QPDFValue static std::shared_ptr create(QPDF* qpdf, QPDFObjGen const& og); std::shared_ptr 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); diff --git a/qpdf/qtest/many-nulls.test b/qpdf/qtest/many-nulls.test index 8a723d53..26ce5f8a 100644 --- a/qpdf/qtest/many-nulls.test +++ b/qpdf/qtest/many-nulls.test @@ -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); diff --git a/qpdf/qtest/qpdf-json.test b/qpdf/qtest/qpdf-json.test index 9542bccf..299bcd7e 100644 --- a/qpdf/qtest/qpdf-json.test +++ b/qpdf/qtest/qpdf-json.test @@ -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); diff --git a/qpdf/qtest/qpdf/minimal-nulls-1.json b/qpdf/qtest/qpdf/minimal-nulls-1.json new file mode 100644 index 00000000..0b8f6e53 --- /dev/null +++ b/qpdf/qtest/qpdf/minimal-nulls-1.json @@ -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�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 + } + } + } +} diff --git a/qpdf/qtest/qpdf/minimal-nulls-2.json b/qpdf/qtest/qpdf/minimal-nulls-2.json new file mode 100644 index 00000000..6bf61ea7 --- /dev/null +++ b/qpdf/qtest/qpdf/minimal-nulls-2.json @@ -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 + } + } + } + ] +} diff --git a/qpdf/qtest/qpdf/minimal-nulls.pdf b/qpdf/qtest/qpdf/minimal-nulls.pdf new file mode 100644 index 00000000..491d0b30 --- /dev/null +++ b/qpdf/qtest/qpdf/minimal-nulls.pdf @@ -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 [<31415926535897932384626433832795>] +>> +startxref +3201 +%%EOF diff --git a/qpdf/qtest/qpdf/weird-tokens-alt.json b/qpdf/qtest/qpdf/weird-tokens-alt.json index 607bdd55..7fbff908 100644 --- a/qpdf/qtest/qpdf/weird-tokens-alt.json +++ b/qpdf/qtest/qpdf/weird-tokens-alt.json @@ -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 diff --git a/qpdf/qtest/qpdf/weird-tokens-v1.json b/qpdf/qtest/qpdf/weird-tokens-v1.json new file mode 100644 index 00000000..8b8c194c --- /dev/null +++ b/qpdf/qtest/qpdf/weird-tokens-v1.json @@ -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 + } + } + } +} diff --git a/qpdf/qtest/qpdf/weird-tokens.json b/qpdf/qtest/qpdf/weird-tokens.json index 6aca6a5a..4ad926e7 100644 --- a/qpdf/qtest/qpdf/weird-tokens.json +++ b/qpdf/qtest/qpdf/weird-tokens.json @@ -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 diff --git a/qpdf/qtest/qpdf/weird-tokens.pdf b/qpdf/qtest/qpdf/weird-tokens.pdf index 27415a46..c49a046f 100644 --- a/qpdf/qtest/qpdf/weird-tokens.pdf +++ b/qpdf/qtest/qpdf/weird-tokens.pdf @@ -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 diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index e7451576..565b517d 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -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()) {