aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt6
-rw-r--r--ChangeLog17
-rw-r--r--README-maintainer.md31
-rw-r--r--TODO.md3
-rw-r--r--include/qpdf/JSON.hh5
-rw-r--r--include/qpdf/QPDF.hh13
-rw-r--r--include/qpdf/QPDFFormFieldObjectHelper.hh4
-rw-r--r--include/qpdf/QPDFJob.hh1
-rw-r--r--include/qpdf/QPDFObjectHandle.hh9
-rw-r--r--job.sums2
-rw-r--r--libqpdf/JSON.cc8
-rw-r--r--libqpdf/QPDFFormFieldObjectHelper.cc30
-rw-r--r--libqpdf/QPDFJob.cc37
-rw-r--r--libqpdf/QPDFObjectHandle.cc26
-rw-r--r--libqpdf/QPDF_Array.cc32
-rw-r--r--libqpdf/QPDF_Bool.cc8
-rw-r--r--libqpdf/QPDF_Destroyed.cc7
-rw-r--r--libqpdf/QPDF_Dictionary.cc31
-rw-r--r--libqpdf/QPDF_InlineImage.cc8
-rw-r--r--libqpdf/QPDF_Integer.cc7
-rw-r--r--libqpdf/QPDF_Name.cc74
-rw-r--r--libqpdf/QPDF_Null.cc8
-rw-r--r--libqpdf/QPDF_Operator.cc8
-rw-r--r--libqpdf/QPDF_Real.cc19
-rw-r--r--libqpdf/QPDF_Reserved.cc5
-rw-r--r--libqpdf/QPDF_Stream.cc143
-rw-r--r--libqpdf/QPDF_String.cc42
-rw-r--r--libqpdf/QPDF_Unresolved.cc5
-rw-r--r--libqpdf/QPDF_json.cc168
-rw-r--r--libqpdf/qpdf/JSON_writer.hh137
-rw-r--r--libqpdf/qpdf/QPDFObject_private.hh6
-rw-r--r--libqpdf/qpdf/QPDFValue.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Array.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Bool.hh3
-rw-r--r--libqpdf/qpdf/QPDF_Destroyed.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Dictionary.hh2
-rw-r--r--libqpdf/qpdf/QPDF_InlineImage.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Integer.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Name.hh7
-rw-r--r--libqpdf/qpdf/QPDF_Null.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Operator.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Real.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Reserved.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Stream.hh10
-rw-r--r--libqpdf/qpdf/QPDF_String.hh2
-rw-r--r--libqpdf/qpdf/QPDF_Unresolved.hh2
-rw-r--r--manual/installation.rst4
-rw-r--r--manual/release-notes.rst4
-rw-r--r--qpdf/qtest/many-nulls.test19
-rw-r--r--qpdf/qtest/qpdf-json.test15
-rw-r--r--qpdf/qtest/qpdf/button-set-out.pdf6
-rw-r--r--qpdf/qtest/qpdf/button-set.pdf2
-rw-r--r--qpdf/qtest/qpdf/minimal-nulls-1.json453
-rw-r--r--qpdf/qtest/qpdf/minimal-nulls-2.json424
-rw-r--r--qpdf/qtest/qpdf/minimal-nulls.pdf387
-rw-r--r--qpdf/qtest/qpdf/weird-tokens-alt.json136
-rw-r--r--qpdf/qtest/qpdf/weird-tokens-v1.json295
-rw-r--r--qpdf/qtest/qpdf/weird-tokens.json136
-rw-r--r--qpdf/qtest/qpdf/weird-tokens.pdf148
-rw-r--r--qpdf/test_driver.cc22
60 files changed, 2646 insertions, 349 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 89a28302..8a3d5f5a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -72,6 +72,7 @@ CMAKE_DEPENDENT_OPTION(
BUILD_DOC_DIST "Create distribution of manual" ON
"BUILD_DOC_PDF;BUILD_DOC_HTML" OFF)
+option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
option(BUILD_SHARED_LIBS "Build qpdf shared libraries" ON)
option(BUILD_STATIC_LIBS "Build qpdf static libraries" ON)
option(QTEST_COLOR "Whether qtest's output should be in color" ON)
@@ -296,6 +297,11 @@ set(CPACK_RESOURCE_FILE_LICENSE "${qpdf_SOURCE_DIR}/LICENSE.txt")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://qpdf.sourceforge.io/")
set(CPACK_NSIS_MUI_ICON "${qpdf_SOURCE_DIR}/logo/qpdf.ico")
+if(ENABLE_COVERAGE)
+ add_compile_options(--coverage -O0)
+ add_link_options(--coverage)
+endif()
+
include(CPack)
# Install components -- documented in _installation in
diff --git a/ChangeLog b/ChangeLog
index 7f673204..890d0f99 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,20 @@
+2024-02-17 Jay Berkenbilt <ejb@ql.org>
+
+ * Add ENABLE_COVERAGE cmake option to assist with generating
+ coverage reports.
+
+ * From M. Holger: add QPDFObjectHandle::writeJSON to directly
+ write a JSON representation to a pipeline. This is much faster
+ than writing the serialized result of getJSON.
+
+2024-02-11 Jay Berkenbilt <ejb@ql.org>
+
+ * The previous fix to #1056 was incomplete. When setting a check
+ box value, the previous fix allowed any value other than /Off to
+ mean checked. Now we also set the actual value based on the
+ allowable non-/Off value in the normal appearance dictionary.
+ Fixes #1056.
+
2024-02-03 Jay Berkenbilt <ejb@ql.org>
* Add fuzz testing for JSON.
diff --git a/README-maintainer.md b/README-maintainer.md
index ca28317a..df1656b3 100644
--- a/README-maintainer.md
+++ b/README-maintainer.md
@@ -25,19 +25,19 @@
**Remember to check pull requests as well as issues in github.**
+Include `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` with cmake if using emacs lsp mode.
+
Default:
```
-cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
- -DMAINTAINER_MODE=1 -DBUILD_STATIC_LIBS=0 \
+cmake -DMAINTAINER_MODE=ON -DBUILD_STATIC_LIBS=OFF \
-DCMAKE_BUILD_TYPE=RelWithDebInfo ..
```
Debugging:
```
-cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
- -DMAINTAINER_MODE=1 -DBUILD_SHARED_LIBS=0 \
+cmake -DMAINTAINER_MODE=ON -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=Debug ..
```
@@ -45,13 +45,26 @@ Profiling:
```
CFLAGS=-pg LDFLAGS=-pg \
- cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
- -DMAINTAINER_MODE=1 -DBUILD_SHARED_LIBS=0 \
+ cmake -DMAINTAINER_MODE=ON -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=Debug ..
```
Then run `gprof gmon.out`. Note that gmon.out is not cumulative.
+Coverage:
+
+```
+cmake -DMAINTAINER_MODE=ON -DBUILD_SHARED_LIBS=OFF \
+ -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON..
+```
+
+Then, from the build directory, run the test suite (`ctest --verbose`) followed by
+```
+gcovr -r .. --html --html-details -o coverage-report.html
+```
+
+Note that, in early 2024, branch coverage information is not very accurate with C++.
+
Memory checks:
```
@@ -59,8 +72,7 @@ CFLAGS="-fsanitize=address -fsanitize=undefined" \
CXXFLAGS="-fsanitize=address -fsanitize=undefined" \
LDFLAGS="-fsanitize=address -fsanitize=undefined" \
CC=clang CXX=clang++ \
- cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
- -DMAINTAINER_MODE=1 -DBUILD_SHARED_LIBS=0 \
+ cmake -DMAINTAINER_MODE=ON -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=Debug ..
```
@@ -691,8 +703,7 @@ export QPDF_BUILD_LIBDIR=$QPDF_SOURCE_TREE/build/libqpdf
export LD_LIBRARY_PATH=$QPDF_BUILD_LIBDIR
cd qpdf
mkdir build
-cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
- -DMAINTAINER_MODE=1 -DBUILD_STATIC_LIBS=0 \
+cmake -B build -DMAINTAINER_MODE=ON -DBUILD_STATIC_LIBS=OFF \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cat <<'EOF'
#!/bin/bash
diff --git a/TODO.md b/TODO.md
index 34193460..8baa989d 100644
--- a/TODO.md
+++ b/TODO.md
@@ -38,6 +38,9 @@ Next
* Make it possible to see incremental updates in qdf mode.
* Make it possible to add incremental updates.
* We may want a writing mode that preserves object IDs. See #339.
+ * Issue #1148 raises concerns about mixing xref tables and xref streams. We will have to consider
+ how qpdf should deal with this while making sure not to break hybrid-ref files, which are in the
+ test suite.
* Support digital signatures. This probably requires support for incremental updates. First, add
support for verifying digital signatures. Then we can consider adding support for signing
documents, though the ability to sign documents is less useful without an interactive process of
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<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
diff --git a/include/qpdf/QPDFFormFieldObjectHelper.hh b/include/qpdf/QPDFFormFieldObjectHelper.hh
index f31bb7bd..881a7db4 100644
--- a/include/qpdf/QPDFFormFieldObjectHelper.hh
+++ b/include/qpdf/QPDFFormFieldObjectHelper.hh
@@ -166,7 +166,9 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
// either /Tx (text) or /Ch (choice), set /NeedAppearances to true. You can explicitly tell this
// method not to set /NeedAppearances if you are going to generate an appearance stream
// yourself. Starting with qpdf 8.3.0, this method handles fields of type /Btn (checkboxes,
- // radio buttons, pushbuttons) specially.
+ // radio buttons, pushbuttons) specially. When setting a checkbox value, any value other than
+ // /Off will be treated as on, and the actual value set will be based on the appearance stream's
+ // /N dictionary, so the value that ends up in /V may not exactly match the value you pass in.
QPDF_DLL
void setV(QPDFObjectHandle value, bool need_appearances = true);
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/job.sums b/job.sums
index 1da9f07b..dddf5fa3 100644
--- a/job.sums
+++ b/job.sums
@@ -1,5 +1,5 @@
# Generated by generate_auto_job
-CMakeLists.txt f53d67f8c6ace1a2fb63dd9a963ead6b5cd698556f9d0adef2c10744a565b54f
+CMakeLists.txt 9bfd82a7d225b88760ff5af211f9af35a0d9fcdd40fa15fdf7fc820944c2d5f9
generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a86
include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4
include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
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 <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");
}
diff --git a/libqpdf/QPDFFormFieldObjectHelper.cc b/libqpdf/QPDFFormFieldObjectHelper.cc
index 371ed271..40627c3d 100644
--- a/libqpdf/QPDFFormFieldObjectHelper.cc
+++ b/libqpdf/QPDFFormFieldObjectHelper.cc
@@ -310,8 +310,8 @@ QPDFFormFieldObjectHelper::setV(QPDFObjectHandle value, bool need_appearances)
setCheckBoxValue((name != "/Off"));
}
if (!okay) {
- this->oh.warnIfPossible("ignoring attempt to set a checkbox field to a value of "
- "other than /Yes or /Off");
+ this->oh.warnIfPossible(
+ "ignoring attempt to set a checkbox field to a value whose type is not name");
}
} else if (isRadioButton()) {
if (value.isName()) {
@@ -415,9 +415,6 @@ QPDFFormFieldObjectHelper::setRadioButtonValue(QPDFObjectHandle name)
void
QPDFFormFieldObjectHelper::setCheckBoxValue(bool value)
{
- // Set /AS to /Yes or /Off in addition to setting /V.
- QPDFObjectHandle name = QPDFObjectHandle::newName(value ? "/Yes" : "/Off");
- setFieldAttribute("/V", name);
QPDFObjectHandle AP = this->oh.getKey("/AP");
QPDFObjectHandle annot;
if (AP.isNull()) {
@@ -439,6 +436,29 @@ QPDFFormFieldObjectHelper::setCheckBoxValue(bool value)
} else {
annot = this->oh;
}
+ std::string on_value;
+ if (value) {
+ // Set the "on" value to the first value in the appearance stream's normal state dictionary
+ // that isn't /Off. If not found, fall back to /Yes.
+ if (AP.isDictionary()) {
+ auto N = AP.getKey("/N");
+ if (N.isDictionary()) {
+ for (auto const& iter: N.ditems()) {
+ if (iter.first != "/Off") {
+ on_value = iter.first;
+ break;
+ }
+ }
+ }
+ }
+ if (on_value.empty()) {
+ on_value = "/Yes";
+ }
+ }
+
+ // Set /AS to the on value or /Off in addition to setting /V.
+ QPDFObjectHandle name = QPDFObjectHandle::newName(value ? on_value : "/Off");
+ setFieldAttribute("/V", name);
if (!annot.isInitialized()) {
QTC::TC("qpdf", "QPDFObjectHandle broken checkbox");
this->oh.warnIfPossible("unable to set the value of this checkbox");
diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc
index a699e38d..a85ff0a2 100644
--- a/libqpdf/QPDFJob.cc
+++ b/libqpdf/QPDFJob.cc
@@ -955,23 +955,6 @@ QPDFJob::getWantedJSONObjects()
}
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)
{
if (m->json_version == 1) {
@@ -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 <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,
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 <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
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 <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
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 <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
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 <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";
}
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 <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
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 <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)
+{
+ 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 {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) {
- return JSON::makeString(normalizeName(this->name));
+ p << "\"" << JSON::Writer::encode_string(normalizeName(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);
+ if (auto res = analyzeJSONEncoding(name); res.first) {
+ if (res.second) {
+ p << "\"" << name << "\"";
+ } else {
+ p << "\"" << JSON::Writer::encode_string(name) << "\"";
+ }
} else {
- return JSON::makeString("n:" + normalizeName(this->name));
+ 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 <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";
}
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 <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";
}
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 <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);
}
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 <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
@@ -193,77 +191,108 @@ QPDF_Stream::getStreamJSON(
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 <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
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 <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;
- }
- 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);
+ jw << "{";
+ } else if (!first_key) {
+ jw << ",";
+ }
+ 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);
- }
- JSON::writeDictionaryClose(p, first_qpdf_inner, depth_qpdf);
- JSON::writeArrayClose(p, first_qpdf, depth_top);
+ if (!first) {
+ jw << "\n },";
+ }
+ jw << "\n \"trailer\": {\n \"value\": ";
+ getTrailer().writeJSON(version, jw, true);
+ first = false;
+ }
+ 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 <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
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<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
{
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<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
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<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:
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<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:
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<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
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<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
{
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<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:
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<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
{
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<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();
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<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
{
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<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
{
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<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();
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<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);
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<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
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<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);
diff --git a/manual/installation.rst b/manual/installation.rst
index 7f95f3d9..e49a87eb 100644
--- a/manual/installation.rst
+++ b/manual/installation.rst
@@ -302,6 +302,10 @@ CHECK_SIZES
that ensures an exact match between classes in ``sizes.cc`` and
classes in the library's public API. This option requires Python 3.
+ENABLE_COVERAGE
+ Compile with ``--coverage``. See README-maintainer.md for
+ information about generating coverage reports.
+
ENABLE_QTC
This is off by default, except in maintainer mode. When off,
``QTC::TC`` calls are compiled out by having ``QTC::TC`` be an empty
diff --git a/manual/release-notes.rst b/manual/release-notes.rst
index b9f7d14b..209e9181 100644
--- a/manual/release-notes.rst
+++ b/manual/release-notes.rst
@@ -60,6 +60,10 @@ Planned changes for future 12.x (subject to change):
- Add ``file()``, ``range()``, and ``password()`` to
``QPDFJob::PagesConfig`` as an alternative to ``pageSpec``.
+ - Add ``QPDFObjectHandle::writeJSON`` to write the JSON
+ representation of the object directly to a pipeline. This is
+ much faster than calling ``QPDFObjectHandle::getJSON``.
+
11.8.0: January 8, 2024
- Bug fixes:
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/button-set-out.pdf b/qpdf/qtest/qpdf/button-set-out.pdf
index b7ceae01..d08f4586 100644
--- a/qpdf/qtest/qpdf/button-set-out.pdf
+++ b/qpdf/qtest/qpdf/button-set-out.pdf
@@ -122,7 +122,7 @@ endobj
>>
/P 15 0 R
/T (checkbox1)
- /V /Yes
+ /V /Yup
>>
endobj
@@ -589,10 +589,10 @@ endobj
/AP <<
/N <<
/Off 64 0 R
- /Yes 66 0 R
+ /Yup 66 0 R
>>
>>
- /AS /Yes
+ /AS /Yup
/F 4
/Rect [
118.649
diff --git a/qpdf/qtest/qpdf/button-set.pdf b/qpdf/qtest/qpdf/button-set.pdf
index fe96f336..ca65395f 100644
--- a/qpdf/qtest/qpdf/button-set.pdf
+++ b/qpdf/qtest/qpdf/button-set.pdf
@@ -3481,7 +3481,7 @@ endobj
/AP <<
/N <<
/Off 24 0 R
- /Yes 26 0 R
+ /Yup 26 0 R
>>
>>
/AS /Off
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 [<cfee6745ad454ddb88cadfa224b20523><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 28d8062c..565b517d 100644
--- a/qpdf/test_driver.cc
+++ b/qpdf/test_driver.cc
@@ -1942,7 +1942,9 @@ test_51(QPDF& pdf, char const* arg2)
} else if (Tval == "checkbox1") {
std::cout << "turning checkbox1 on\n";
QPDFFormFieldObjectHelper foh(field);
- foh.setV(QPDFObjectHandle::newName("/Yes"));
+ // The value that eventually gets set is based on what's allowed in /N and may not match
+ // this value.
+ foh.setV(QPDFObjectHandle::newName("/Sure"));
} else if (Tval == "checkbox2") {
std::cout << "turning checkbox2 off\n";
QPDFFormFieldObjectHelper foh(field);
@@ -3380,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)
{
@@ -3481,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()) {