#include <qpdf/QPDFObjectHandle.hh>

#include <qpdf/BufferInputSource.hh>
#include <qpdf/Pl_Buffer.hh>
#include <qpdf/Pl_QPDFTokenizer.hh>
#include <qpdf/QPDF.hh>
#include <qpdf/QPDFExc.hh>
#include <qpdf/QPDFLogger.hh>
#include <qpdf/QPDFMatrix.hh>
#include <qpdf/QPDFObject_private.hh>
#include <qpdf/QPDFPageObjectHelper.hh>
#include <qpdf/QPDFParser.hh>
#include <qpdf/QPDF_Array.hh>
#include <qpdf/QPDF_Bool.hh>
#include <qpdf/QPDF_Dictionary.hh>
#include <qpdf/QPDF_InlineImage.hh>
#include <qpdf/QPDF_Integer.hh>
#include <qpdf/QPDF_Name.hh>
#include <qpdf/QPDF_Null.hh>
#include <qpdf/QPDF_Operator.hh>
#include <qpdf/QPDF_Real.hh>
#include <qpdf/QPDF_Reserved.hh>
#include <qpdf/QPDF_Stream.hh>
#include <qpdf/QPDF_String.hh>
#include <qpdf/QPDF_Unresolved.hh>
#include <qpdf/SparseOHArray.hh>

#include <qpdf/QIntC.hh>
#include <qpdf/QTC.hh>
#include <qpdf/QUtil.hh>

#include <algorithm>
#include <cstring>
#include <ctype.h>
#include <limits.h>
#include <stdexcept>
#include <stdlib.h>

using namespace std::literals;

namespace
{
    class TerminateParsing
    {
    };
} // namespace

QPDFObjectHandle::StreamDataProvider::StreamDataProvider(bool supports_retry) :
    supports_retry(supports_retry)
{
}

QPDFObjectHandle::StreamDataProvider::~StreamDataProvider()
{
    // Must be explicit and not inline -- see QPDF_DLL_CLASS in
    // README-maintainer
}

void
QPDFObjectHandle::StreamDataProvider::provideStreamData(
    QPDFObjGen const& og, Pipeline* pipeline)
{
    return provideStreamData(og.getObj(), og.getGen(), pipeline);
}

bool
QPDFObjectHandle::StreamDataProvider::provideStreamData(
    QPDFObjGen const& og,
    Pipeline* pipeline,
    bool suppress_warnings,
    bool will_retry)
{
    return provideStreamData(
        og.getObj(), og.getGen(), pipeline, suppress_warnings, will_retry);
}

void
QPDFObjectHandle::StreamDataProvider::provideStreamData(
    int objid, int generation, Pipeline* pipeline)
{
    throw std::logic_error(
        "you must override provideStreamData -- see QPDFObjectHandle.hh");
}

bool
QPDFObjectHandle::StreamDataProvider::provideStreamData(
    int objid,
    int generation,
    Pipeline* pipeline,
    bool suppress_warnings,
    bool will_retry)
{
    throw std::logic_error(
        "you must override provideStreamData -- see QPDFObjectHandle.hh");
    return false;
}

bool
QPDFObjectHandle::StreamDataProvider::supportsRetry()
{
    return this->supports_retry;
}

namespace
{
    class CoalesceProvider: public QPDFObjectHandle::StreamDataProvider
    {
      public:
        CoalesceProvider(
            QPDFObjectHandle containing_page, QPDFObjectHandle old_contents) :
            containing_page(containing_page),
            old_contents(old_contents)
        {
        }
        virtual ~CoalesceProvider() = default;
        virtual void provideStreamData(QPDFObjGen const&, Pipeline* pipeline);

      private:
        QPDFObjectHandle containing_page;
        QPDFObjectHandle old_contents;
    };
} // namespace

void
CoalesceProvider::provideStreamData(QPDFObjGen const&, Pipeline* p)
{
    QTC::TC("qpdf", "QPDFObjectHandle coalesce provide stream data");
    std::string description =
        "page object " + containing_page.getObjGen().unparse(' ');
    std::string all_description;
    old_contents.pipeContentStreams(p, description, all_description);
}

void
QPDFObjectHandle::TokenFilter::handleEOF()
{
}

void
QPDFObjectHandle::TokenFilter::setPipeline(Pipeline* p)
{
    this->pipeline = p;
}

void
QPDFObjectHandle::TokenFilter::write(char const* data, size_t len)
{
    if (!this->pipeline) {
        return;
    }
    if (len) {
        this->pipeline->write(data, len);
    }
}

void
QPDFObjectHandle::TokenFilter::write(std::string const& str)
{
    write(str.c_str(), str.length());
}

void
QPDFObjectHandle::TokenFilter::writeToken(QPDFTokenizer::Token const& token)
{
    std::string value = token.getRawValue();
    write(value.c_str(), value.length());
}

void
QPDFObjectHandle::ParserCallbacks::handleObject(QPDFObjectHandle)
{
    throw std::logic_error("You must override one of the"
                           " handleObject methods in ParserCallbacks");
}

void
QPDFObjectHandle::ParserCallbacks::handleObject(
    QPDFObjectHandle oh, size_t, size_t)
{
    // This version of handleObject was added in qpdf 9. If the
    // developer did not override it, fall back to the older
    // interface.
    handleObject(oh);
}

void
QPDFObjectHandle::ParserCallbacks::contentSize(size_t)
{
    // Ignore by default; overriding this is optional.
}

void
QPDFObjectHandle::ParserCallbacks::terminateParsing()
{
    throw TerminateParsing();
}

namespace
{
    class LastChar: public Pipeline
    {
      public:
        LastChar(Pipeline* next);
        virtual ~LastChar() = default;
        virtual void write(unsigned char const* data, size_t len);
        virtual void finish();
        unsigned char getLastChar();

      private:
        unsigned char last_char;
    };
} // namespace

LastChar::LastChar(Pipeline* next) :
    Pipeline("lastchar", next),
    last_char(0)
{
}

void
LastChar::write(unsigned char const* data, size_t len)
{
    if (len > 0) {
        this->last_char = data[len - 1];
    }
    getNext()->write(data, len);
}

void
LastChar::finish()
{
    getNext()->finish();
}

unsigned char
LastChar::getLastChar()
{
    return this->last_char;
}

bool
QPDFObjectHandle::isSameObjectAs(QPDFObjectHandle const& rhs) const
{
    return this->obj == rhs.obj;
}

void
QPDFObjectHandle::disconnect()
{
    // Recursively remove association with any QPDF object. This
    // method may only be called during final destruction.
    // QPDF::~QPDF() calls it for indirect objects using the object
    // pointer itself, so we don't do that here. Other objects call it
    // through this method.
    if (!isIndirect()) {
        this->obj->disconnect();
    }
}

qpdf_object_type_e
QPDFObjectHandle::getTypeCode()
{
    return dereference() ? this->obj->getTypeCode() : ::ot_uninitialized;
}

char const*
QPDFObjectHandle::getTypeName()
{
    return dereference() ? this->obj->getTypeName() : "uninitialized";
}

QPDF_Array*
QPDFObjectHandle::asArray()
{
    return dereference() ? obj->as<QPDF_Array>() : nullptr;
}

QPDF_Bool*
QPDFObjectHandle::asBool()
{
    return dereference() ? obj->as<QPDF_Bool>() : nullptr;
}

QPDF_Dictionary*
QPDFObjectHandle::asDictionary()
{
    return dereference() ? obj->as<QPDF_Dictionary>() : nullptr;
}

QPDF_InlineImage*
QPDFObjectHandle::asInlineImage()
{
    return dereference() ? obj->as<QPDF_InlineImage>() : nullptr;
}

QPDF_Integer*
QPDFObjectHandle::asInteger()
{
    return dereference() ? obj->as<QPDF_Integer>() : nullptr;
}

QPDF_Name*
QPDFObjectHandle::asName()
{
    return dereference() ? obj->as<QPDF_Name>() : nullptr;
}

QPDF_Null*
QPDFObjectHandle::asNull()
{
    return dereference() ? obj->as<QPDF_Null>() : nullptr;
}

QPDF_Operator*
QPDFObjectHandle::asOperator()
{
    return dereference() ? obj->as<QPDF_Operator>() : nullptr;
}

QPDF_Real*
QPDFObjectHandle::asReal()
{
    return dereference() ? obj->as<QPDF_Real>() : nullptr;
}

QPDF_Reserved*
QPDFObjectHandle::asReserved()
{
    return dereference() ? obj->as<QPDF_Reserved>() : nullptr;
}

QPDF_Stream*
QPDFObjectHandle::asStream()
{
    return dereference() ? obj->as<QPDF_Stream>() : nullptr;
}

QPDF_Stream*
QPDFObjectHandle::asStreamWithAssert()
{
    auto stream = asStream();
    assertType("stream", stream);
    return stream;
}

QPDF_String*
QPDFObjectHandle::asString()
{
    return dereference() ? obj->as<QPDF_String>() : nullptr;
}

bool
QPDFObjectHandle::isDestroyed()
{
    return dereference() && (obj->getTypeCode() == ::ot_destroyed);
}

bool
QPDFObjectHandle::isBool()
{
    return dereference() && (obj->getTypeCode() == ::ot_boolean);
}

bool
QPDFObjectHandle::isDirectNull() const
{
    // Don't call dereference() -- this is a const method, and we know
    // objid == 0, so there's nothing to resolve.
    return (
        isInitialized() && (getObjectID() == 0) &&
        (obj->getTypeCode() == ::ot_null));
}

bool
QPDFObjectHandle::isNull()
{
    return dereference() && (obj->getTypeCode() == ::ot_null);
}

bool
QPDFObjectHandle::isInteger()
{
    return dereference() && (obj->getTypeCode() == ::ot_integer);
}

bool
QPDFObjectHandle::isReal()
{
    return dereference() && (obj->getTypeCode() == ::ot_real);
}

bool
QPDFObjectHandle::isNumber()
{
    return (isInteger() || isReal());
}

double
QPDFObjectHandle::getNumericValue()
{
    double result = 0.0;
    if (isInteger()) {
        result = static_cast<double>(getIntValue());
    } else if (isReal()) {
        result = atof(getRealValue().c_str());
    } else {
        typeWarning("number", "returning 0");
        QTC::TC("qpdf", "QPDFObjectHandle numeric non-numeric");
    }
    return result;
}

bool
QPDFObjectHandle::getValueAsNumber(double& value)
{
    if (!isNumber()) {
        return false;
    }
    value = getNumericValue();
    return true;
}

bool
QPDFObjectHandle::isName()
{
    return dereference() && (obj->getTypeCode() == ::ot_name);
}

bool
QPDFObjectHandle::isString()
{
    return dereference() && (obj->getTypeCode() == ::ot_string);
}

bool
QPDFObjectHandle::isOperator()
{
    return dereference() && (obj->getTypeCode() == ::ot_operator);
}

bool
QPDFObjectHandle::isInlineImage()
{
    return dereference() && (obj->getTypeCode() == ::ot_inlineimage);
}

bool
QPDFObjectHandle::isArray()
{
    return dereference() && (obj->getTypeCode() == ::ot_array);
}

bool
QPDFObjectHandle::isDictionary()
{
    return dereference() && (obj->getTypeCode() == ::ot_dictionary);
}

bool
QPDFObjectHandle::isStream()
{
    return dereference() && (obj->getTypeCode() == ::ot_stream);
}

bool
QPDFObjectHandle::isReserved()
{
    return dereference() && (obj->getTypeCode() == ::ot_reserved);
}

bool
QPDFObjectHandle::isScalar()
{
    return isBool() || isInteger() || isName() || isNull() || isReal() ||
        isString();
}

bool
QPDFObjectHandle::isNameAndEquals(std::string const& name)
{
    return isName() && (getName() == name);
}

bool
QPDFObjectHandle::isDictionaryOfType(
    std::string const& type, std::string const& subtype)
{
    return isDictionary() &&
        (type.empty() || getKey("/Type").isNameAndEquals(type)) &&
        (subtype.empty() || getKey("/Subtype").isNameAndEquals(subtype));
}

bool
QPDFObjectHandle::isStreamOfType(
    std::string const& type, std::string const& subtype)
{
    return isStream() && getDict().isDictionaryOfType(type, subtype);
}

// Bool accessors

bool
QPDFObjectHandle::getBoolValue()
{
    auto boolean = asBool();
    if (boolean) {
        return boolean->getVal();
    } else {
        typeWarning("boolean", "returning false");
        QTC::TC("qpdf", "QPDFObjectHandle boolean returning false");
        return false;
    }
}

bool
QPDFObjectHandle::getValueAsBool(bool& value)
{
    auto boolean = asBool();
    if (boolean == nullptr) {
        return false;
    }
    value = boolean->getVal();
    return true;
}

// Integer accessors

long long
QPDFObjectHandle::getIntValue()
{
    auto integer = asInteger();
    if (integer) {
        return integer->getVal();
    } else {
        typeWarning("integer", "returning 0");
        QTC::TC("qpdf", "QPDFObjectHandle integer returning 0");
        return 0;
    }
}

bool
QPDFObjectHandle::getValueAsInt(long long& value)
{
    auto integer = asInteger();
    if (integer == nullptr) {
        return false;
    }
    value = integer->getVal();
    return true;
}

int
QPDFObjectHandle::getIntValueAsInt()
{
    int result = 0;
    long long v = getIntValue();
    if (v < INT_MIN) {
        QTC::TC("qpdf", "QPDFObjectHandle int returning INT_MIN");
        warnIfPossible(
            "requested value of integer is too small; returning INT_MIN");
        result = INT_MIN;
    } else if (v > INT_MAX) {
        QTC::TC("qpdf", "QPDFObjectHandle int returning INT_MAX");
        warnIfPossible(
            "requested value of integer is too big; returning INT_MAX");
        result = INT_MAX;
    } else {
        result = static_cast<int>(v);
    }
    return result;
}

bool
QPDFObjectHandle::getValueAsInt(int& value)
{
    if (!isInteger()) {
        return false;
    }
    value = getIntValueAsInt();
    return true;
}

unsigned long long
QPDFObjectHandle::getUIntValue()
{
    unsigned long long result = 0;
    long long v = getIntValue();
    if (v < 0) {
        QTC::TC("qpdf", "QPDFObjectHandle uint returning 0");
        warnIfPossible(
            "unsigned value request for negative number; returning 0");
    } else {
        result = static_cast<unsigned long long>(v);
    }
    return result;
}

bool
QPDFObjectHandle::getValueAsUInt(unsigned long long& value)
{
    if (!isInteger()) {
        return false;
    }
    value = getUIntValue();
    return true;
}

unsigned int
QPDFObjectHandle::getUIntValueAsUInt()
{
    unsigned int result = 0;
    long long v = getIntValue();
    if (v < 0) {
        QTC::TC("qpdf", "QPDFObjectHandle uint uint returning 0");
        warnIfPossible(
            "unsigned integer value request for negative number; returning 0");
        result = 0;
    } else if (v > UINT_MAX) {
        QTC::TC("qpdf", "QPDFObjectHandle uint returning UINT_MAX");
        warnIfPossible("requested value of unsigned integer is too big;"
                       " returning UINT_MAX");
        result = UINT_MAX;
    } else {
        result = static_cast<unsigned int>(v);
    }
    return result;
}

bool
QPDFObjectHandle::getValueAsUInt(unsigned int& value)
{
    if (!isInteger()) {
        return false;
    }
    value = getUIntValueAsUInt();
    return true;
}

// Real accessors

std::string
QPDFObjectHandle::getRealValue()
{
    if (isReal()) {
        return obj->getStringValue();
    } else {
        typeWarning("real", "returning 0.0");
        QTC::TC("qpdf", "QPDFObjectHandle real returning 0.0");
        return "0.0";
    }
}

bool
QPDFObjectHandle::getValueAsReal(std::string& value)
{
    if (!isReal()) {
        return false;
    }
    value = obj->getStringValue();
    return true;
}

// Name accessors

std::string
QPDFObjectHandle::getName()
{
    if (isName()) {
        return obj->getStringValue();
    } else {
        typeWarning("name", "returning dummy name");
        QTC::TC("qpdf", "QPDFObjectHandle name returning dummy name");
        return "/QPDFFakeName";
    }
}

bool
QPDFObjectHandle::getValueAsName(std::string& value)
{
    if (!isName()) {
        return false;
    }
    value = obj->getStringValue();
    return true;
}

// String accessors

std::string
QPDFObjectHandle::getStringValue()
{
    if (isString()) {
        return obj->getStringValue();
    } else {
        typeWarning("string", "returning empty string");
        QTC::TC("qpdf", "QPDFObjectHandle string returning empty string");
        return "";
    }
}

bool
QPDFObjectHandle::getValueAsString(std::string& value)
{
    if (!isString()) {
        return false;
    }
    value = obj->getStringValue();
    return true;
}

std::string
QPDFObjectHandle::getUTF8Value()
{
    auto str = asString();
    if (str) {
        return str->getUTF8Val();
    } else {
        typeWarning("string", "returning empty string");
        QTC::TC("qpdf", "QPDFObjectHandle string returning empty utf8");
        return "";
    }
}

bool
QPDFObjectHandle::getValueAsUTF8(std::string& value)
{
    auto str = asString();
    if (str == nullptr) {
        return false;
    }
    value = str->getUTF8Val();
    return true;
}

// Operator and Inline Image accessors

std::string
QPDFObjectHandle::getOperatorValue()
{
    if (isOperator()) {
        return obj->getStringValue();
    } else {
        typeWarning("operator", "returning fake value");
        QTC::TC("qpdf", "QPDFObjectHandle operator returning fake value");
        return "QPDFFAKE";
    }
}

bool
QPDFObjectHandle::getValueAsOperator(std::string& value)
{
    if (!isOperator()) {
        return false;
    }
    value = obj->getStringValue();
    return true;
}

std::string
QPDFObjectHandle::getInlineImageValue()
{
    if (isInlineImage()) {
        return obj->getStringValue();
    } else {
        typeWarning("inlineimage", "returning empty data");
        QTC::TC("qpdf", "QPDFObjectHandle inlineimage returning empty data");
        return "";
    }
}

bool
QPDFObjectHandle::getValueAsInlineImage(std::string& value)
{
    if (!isInlineImage()) {
        return false;
    }
    value = obj->getStringValue();
    return true;
}

// Array accessors

QPDFObjectHandle::QPDFArrayItems
QPDFObjectHandle::aitems()
{
    return QPDFArrayItems(*this);
}

int
QPDFObjectHandle::getArrayNItems()
{
    auto array = asArray();
    if (array) {
        return array->getNItems();
    } else {
        typeWarning("array", "treating as empty");
        QTC::TC("qpdf", "QPDFObjectHandle array treating as empty");
        return 0;
    }
}

QPDFObjectHandle
QPDFObjectHandle::getArrayItem(int n)
{
    auto array = asArray();
    if (array && (n < array->getNItems()) && (n >= 0)) {
        return array->getItem(n);
    } else {
        if (array) {
            objectWarning("returning null for out of bounds array access");
            QTC::TC("qpdf", "QPDFObjectHandle array bounds");
        } else {
            typeWarning("array", "returning null");
            QTC::TC("qpdf", "QPDFObjectHandle array null for non-array");
        }
        static auto constexpr msg =
            " -> null returned from invalid array access"sv;
        return QPDF_Null::create(obj, msg, "");
    }
}

bool
QPDFObjectHandle::isRectangle()
{
    auto array = asArray();
    if ((array == nullptr) || (array->getNItems() != 4)) {
        return false;
    }
    for (int i = 0; i < 4; ++i) {
        if (!array->getItem(i).isNumber()) {
            return false;
        }
    }
    return true;
}

bool
QPDFObjectHandle::isMatrix()
{
    auto array = asArray();
    if ((array == nullptr) || (array->getNItems() != 6)) {
        return false;
    }
    for (int i = 0; i < 6; ++i) {
        if (!array->getItem(i).isNumber()) {
            return false;
        }
    }
    return true;
}

QPDFObjectHandle::Rectangle
QPDFObjectHandle::getArrayAsRectangle()
{
    Rectangle result;
    if (isRectangle()) {
        auto array = asArray();
        // Rectangle coordinates are always supposed to be llx, lly,
        // urx, ury, but files have been found in the wild where
        // llx > urx or lly > ury.
        double i0 = array->getItem(0).getNumericValue();
        double i1 = array->getItem(1).getNumericValue();
        double i2 = array->getItem(2).getNumericValue();
        double i3 = array->getItem(3).getNumericValue();
        result = Rectangle(
            std::min(i0, i2),
            std::min(i1, i3),
            std::max(i0, i2),
            std::max(i1, i3));
    }
    return result;
}

QPDFObjectHandle::Matrix
QPDFObjectHandle::getArrayAsMatrix()
{
    Matrix result;
    if (isMatrix()) {
        auto array = asArray();
        result = Matrix(
            array->getItem(0).getNumericValue(),
            array->getItem(1).getNumericValue(),
            array->getItem(2).getNumericValue(),
            array->getItem(3).getNumericValue(),
            array->getItem(4).getNumericValue(),
            array->getItem(5).getNumericValue());
    }
    return result;
}

std::vector<QPDFObjectHandle>
QPDFObjectHandle::getArrayAsVector()
{
    std::vector<QPDFObjectHandle> result;
    auto array = asArray();
    if (array) {
        array->getAsVector(result);
    } else {
        typeWarning("array", "treating as empty");
        QTC::TC("qpdf", "QPDFObjectHandle array treating as empty vector");
    }
    return result;
}

// Array mutators

void
QPDFObjectHandle::setArrayItem(int n, QPDFObjectHandle const& item)
{
    auto array = asArray();
    if (array) {
        checkOwnership(item);
        array->setItem(n, item);
    } else {
        typeWarning("array", "ignoring attempt to set item");
        QTC::TC("qpdf", "QPDFObjectHandle array ignoring set item");
    }
}

void
QPDFObjectHandle::setArrayFromVector(std::vector<QPDFObjectHandle> const& items)
{
    auto array = asArray();
    if (array) {
        for (auto const& item: items) {
            checkOwnership(item);
        }
        array->setFromVector(items);
    } else {
        typeWarning("array", "ignoring attempt to replace items");
        QTC::TC("qpdf", "QPDFObjectHandle array ignoring replace items");
    }
}

void
QPDFObjectHandle::insertItem(int at, QPDFObjectHandle const& item)
{
    auto array = asArray();
    if (array) {
        array->insertItem(at, item);
    } else {
        typeWarning("array", "ignoring attempt to insert item");
        QTC::TC("qpdf", "QPDFObjectHandle array ignoring insert item");
    }
}

QPDFObjectHandle
QPDFObjectHandle::insertItemAndGetNew(int at, QPDFObjectHandle const& item)
{
    insertItem(at, item);
    return item;
}

void
QPDFObjectHandle::appendItem(QPDFObjectHandle const& item)
{
    auto array = asArray();
    if (array) {
        checkOwnership(item);
        array->appendItem(item);
    } else {
        typeWarning("array", "ignoring attempt to append item");
        QTC::TC("qpdf", "QPDFObjectHandle array ignoring append item");
    }
}

QPDFObjectHandle
QPDFObjectHandle::appendItemAndGetNew(QPDFObjectHandle const& item)
{
    appendItem(item);
    return item;
}

void
QPDFObjectHandle::eraseItem(int at)
{
    auto array = asArray();
    if (array && (at < array->getNItems()) && (at >= 0)) {
        array->eraseItem(at);
    } else {
        if (array) {
            objectWarning("ignoring attempt to erase out of bounds array item");
            QTC::TC("qpdf", "QPDFObjectHandle erase array bounds");
        } else {
            typeWarning("array", "ignoring attempt to erase item");
            QTC::TC("qpdf", "QPDFObjectHandle array ignoring erase item");
        }
    }
}

QPDFObjectHandle
QPDFObjectHandle::eraseItemAndGetOld(int at)
{
    auto result = QPDFObjectHandle::newNull();
    auto array = asArray();
    if (array && (at < array->getNItems()) && (at >= 0)) {
        result = array->getItem(at);
    }
    eraseItem(at);
    return result;
}

// Dictionary accessors

QPDFObjectHandle::QPDFDictItems
QPDFObjectHandle::ditems()
{
    return QPDFDictItems(*this);
}

bool
QPDFObjectHandle::hasKey(std::string const& key)
{
    auto dict = asDictionary();
    if (dict) {
        return dict->hasKey(key);
    } else {
        typeWarning(
            "dictionary", "returning false for a key containment request");
        QTC::TC("qpdf", "QPDFObjectHandle dictionary false for hasKey");
        return false;
    }
}

QPDFObjectHandle
QPDFObjectHandle::getKey(std::string const& key)
{
    if (auto dict = asDictionary()) {
        return dict->getKey(key);
    } else {
        typeWarning("dictionary", "returning null for attempted key retrieval");
        QTC::TC("qpdf", "QPDFObjectHandle dictionary null for getKey");
        static auto constexpr msg =
            " -> null returned from getting key $VD from non-Dictionary"sv;
        return QPDF_Null::create(obj, msg, "");
    }
}

QPDFObjectHandle
QPDFObjectHandle::getKeyIfDict(std::string const& key)
{
    return isNull() ? newNull() : getKey(key);
}

std::set<std::string>
QPDFObjectHandle::getKeys()
{
    std::set<std::string> result;
    auto dict = asDictionary();
    if (dict) {
        result = dict->getKeys();
    } else {
        typeWarning("dictionary", "treating as empty");
        QTC::TC("qpdf", "QPDFObjectHandle dictionary empty set for getKeys");
    }
    return result;
}

std::map<std::string, QPDFObjectHandle>
QPDFObjectHandle::getDictAsMap()
{
    std::map<std::string, QPDFObjectHandle> result;
    auto dict = asDictionary();
    if (dict) {
        result = dict->getAsMap();
    } else {
        typeWarning("dictionary", "treating as empty");
        QTC::TC("qpdf", "QPDFObjectHandle dictionary empty map for asMap");
    }
    return result;
}

// Array and Name accessors
bool
QPDFObjectHandle::isOrHasName(std::string const& value)
{
    if (isNameAndEquals(value)) {
        return true;
    } else if (isArray()) {
        for (auto& item: aitems()) {
            if (item.isNameAndEquals(value)) {
                return true;
            }
        }
    }
    return false;
}

void
QPDFObjectHandle::makeResourcesIndirect(QPDF& owning_qpdf)
{
    if (!isDictionary()) {
        return;
    }
    for (auto const& i1: ditems()) {
        QPDFObjectHandle sub = i1.second;
        if (!sub.isDictionary()) {
            continue;
        }
        for (auto i2: sub.ditems()) {
            std::string const& key = i2.first;
            QPDFObjectHandle val = i2.second;
            if (!val.isIndirect()) {
                sub.replaceKey(key, owning_qpdf.makeIndirectObject(val));
            }
        }
    }
}

void
QPDFObjectHandle::mergeResources(
    QPDFObjectHandle other,
    std::map<std::string, std::map<std::string, std::string>>* conflicts)
{
    if (!(isDictionary() && other.isDictionary())) {
        QTC::TC("qpdf", "QPDFObjectHandle merge top type mismatch");
        return;
    }

    auto make_og_to_name = [](QPDFObjectHandle& dict,
                              std::map<QPDFObjGen, std::string>& og_to_name) {
        for (auto i: dict.ditems()) {
            if (i.second.isIndirect()) {
                og_to_name[i.second.getObjGen()] = i.first;
            }
        }
    };

    // This algorithm is described in comments in QPDFObjectHandle.hh
    // above the declaration of mergeResources.
    for (auto o_top: other.ditems()) {
        std::string const& rtype = o_top.first;
        QPDFObjectHandle other_val = o_top.second;
        if (hasKey(rtype)) {
            QPDFObjectHandle this_val = getKey(rtype);
            if (this_val.isDictionary() && other_val.isDictionary()) {
                if (this_val.isIndirect()) {
                    // Do this even if there are no keys. Various
                    // places in the code call mergeResources with
                    // resource dictionaries that contain empty
                    // subdictionaries just to get this shallow copy
                    // functionality.
                    QTC::TC("qpdf", "QPDFObjectHandle replace with copy");
                    this_val =
                        replaceKeyAndGetNew(rtype, this_val.shallowCopy());
                }
                std::map<QPDFObjGen, std::string> og_to_name;
                std::set<std::string> rnames;
                int min_suffix = 1;
                bool initialized_maps = false;
                for (auto ov_iter: other_val.ditems()) {
                    std::string const& key = ov_iter.first;
                    QPDFObjectHandle rval = ov_iter.second;
                    if (!this_val.hasKey(key)) {
                        if (!rval.isIndirect()) {
                            QTC::TC(
                                "qpdf", "QPDFObjectHandle merge shallow copy");
                            rval = rval.shallowCopy();
                        }
                        this_val.replaceKey(key, rval);
                    } else if (conflicts) {
                        if (!initialized_maps) {
                            make_og_to_name(this_val, og_to_name);
                            rnames = this_val.getResourceNames();
                            initialized_maps = true;
                        }
                        auto rval_og = rval.getObjGen();
                        if (rval.isIndirect() && og_to_name.count(rval_og)) {
                            QTC::TC("qpdf", "QPDFObjectHandle merge reuse");
                            auto new_key = og_to_name[rval_og];
                            if (new_key != key) {
                                (*conflicts)[rtype][key] = new_key;
                            }
                        } else {
                            QTC::TC("qpdf", "QPDFObjectHandle merge generate");
                            std::string new_key = getUniqueResourceName(
                                key + "_", min_suffix, &rnames);
                            (*conflicts)[rtype][key] = new_key;
                            this_val.replaceKey(new_key, rval);
                        }
                    }
                }
            } else if (this_val.isArray() && other_val.isArray()) {
                std::set<std::string> scalars;
                for (auto this_item: this_val.aitems()) {
                    if (this_item.isScalar()) {
                        scalars.insert(this_item.unparse());
                    }
                }
                for (auto other_item: other_val.aitems()) {
                    if (other_item.isScalar()) {
                        if (scalars.count(other_item.unparse()) == 0) {
                            QTC::TC("qpdf", "QPDFObjectHandle merge array");
                            this_val.appendItem(other_item);
                        } else {
                            QTC::TC("qpdf", "QPDFObjectHandle merge array dup");
                        }
                    }
                }
            }
        } else {
            QTC::TC("qpdf", "QPDFObjectHandle merge copy from other");
            replaceKey(rtype, other_val.shallowCopy());
        }
    }
}

std::set<std::string>
QPDFObjectHandle::getResourceNames()
{
    // Return second-level dictionary keys
    std::set<std::string> result;
    if (!isDictionary()) {
        return result;
    }
    for (auto const& key: getKeys()) {
        QPDFObjectHandle val = getKey(key);
        if (val.isDictionary()) {
            for (auto const& val_key: val.getKeys()) {
                result.insert(val_key);
            }
        }
    }
    return result;
}

std::string
QPDFObjectHandle::getUniqueResourceName(
    std::string const& prefix, int& min_suffix, std::set<std::string>* namesp)

{
    std::set<std::string> names = (namesp ? *namesp : getResourceNames());
    int max_suffix = min_suffix + QIntC::to_int(names.size());
    while (min_suffix <= max_suffix) {
        std::string candidate = prefix + std::to_string(min_suffix);
        if (names.count(candidate) == 0) {
            return candidate;
        }
        // Increment after return; min_suffix should be the value
        // used, not the next value.
        ++min_suffix;
    }
    // This could only happen if there is a coding error.
    // The number of candidates we test is more than the
    // number of keys we're checking against.
    throw std::logic_error("unable to find unconflicting name in"
                           " QPDFObjectHandle::getUniqueResourceName");
}

// Dictionary mutators

void
QPDFObjectHandle::replaceKey(
    std::string const& key, QPDFObjectHandle const& value)
{
    auto dict = asDictionary();
    if (dict) {
        checkOwnership(value);
        dict->replaceKey(key, value);
    } else {
        typeWarning("dictionary", "ignoring key replacement request");
        QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring replaceKey");
    }
}

QPDFObjectHandle
QPDFObjectHandle::replaceKeyAndGetNew(
    std::string const& key, QPDFObjectHandle const& value)
{
    replaceKey(key, value);
    return value;
}

QPDFObjectHandle
QPDFObjectHandle::replaceKeyAndGetOld(
    std::string const& key, QPDFObjectHandle const& value)
{
    QPDFObjectHandle old = removeKeyAndGetOld(key);
    replaceKey(key, value);
    return old;
}

void
QPDFObjectHandle::removeKey(std::string const& key)
{
    auto dict = asDictionary();
    if (dict) {
        dict->removeKey(key);
    } else {
        typeWarning("dictionary", "ignoring key removal request");
        QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring removeKey");
    }
}

QPDFObjectHandle
QPDFObjectHandle::removeKeyAndGetOld(std::string const& key)
{
    auto result = QPDFObjectHandle::newNull();
    auto dict = asDictionary();
    if (dict) {
        result = dict->getKey(key);
    }
    removeKey(key);
    return result;
}

void
QPDFObjectHandle::replaceOrRemoveKey(
    std::string const& key, QPDFObjectHandle const& value)
{
    replaceKey(key, value);
}

// Stream accessors
QPDFObjectHandle
QPDFObjectHandle::getDict()
{
    return asStreamWithAssert()->getDict();
}

void
QPDFObjectHandle::setFilterOnWrite(bool val)
{
    asStreamWithAssert()->setFilterOnWrite(val);
}

bool
QPDFObjectHandle::getFilterOnWrite()
{
    return asStreamWithAssert()->getFilterOnWrite();
}

bool
QPDFObjectHandle::isDataModified()
{
    return asStreamWithAssert()->isDataModified();
}

void
QPDFObjectHandle::replaceDict(QPDFObjectHandle const& new_dict)
{
    asStreamWithAssert()->replaceDict(new_dict);
}

std::shared_ptr<Buffer>
QPDFObjectHandle::getStreamData(qpdf_stream_decode_level_e level)
{
    return asStreamWithAssert()->getStreamData(level);
}

std::shared_ptr<Buffer>
QPDFObjectHandle::getRawStreamData()
{
    return asStreamWithAssert()->getRawStreamData();
}

bool
QPDFObjectHandle::pipeStreamData(
    Pipeline* p,
    bool* filtering_attempted,
    int encode_flags,
    qpdf_stream_decode_level_e decode_level,
    bool suppress_warnings,
    bool will_retry)
{
    return asStreamWithAssert()->pipeStreamData(
        p,
        filtering_attempted,
        encode_flags,
        decode_level,
        suppress_warnings,
        will_retry);
}

bool
QPDFObjectHandle::pipeStreamData(
    Pipeline* p,
    int encode_flags,
    qpdf_stream_decode_level_e decode_level,
    bool suppress_warnings,
    bool will_retry)
{
    bool filtering_attempted;
    asStreamWithAssert()->pipeStreamData(
        p,
        &filtering_attempted,
        encode_flags,
        decode_level,
        suppress_warnings,
        will_retry);
    return filtering_attempted;
}

bool
QPDFObjectHandle::pipeStreamData(
    Pipeline* p, bool filter, bool normalize, bool compress)
{
    int encode_flags = 0;
    qpdf_stream_decode_level_e decode_level = qpdf_dl_none;
    if (filter) {
        decode_level = qpdf_dl_generalized;
        if (normalize) {
            encode_flags |= qpdf_ef_normalize;
        }
        if (compress) {
            encode_flags |= qpdf_ef_compress;
        }
    }
    return pipeStreamData(p, encode_flags, decode_level, false);
}

void
QPDFObjectHandle::replaceStreamData(
    std::shared_ptr<Buffer> data,
    QPDFObjectHandle const& filter,
    QPDFObjectHandle const& decode_parms)
{
    asStreamWithAssert()->replaceStreamData(data, filter, decode_parms);
}

void
QPDFObjectHandle::replaceStreamData(
    std::string const& data,
    QPDFObjectHandle const& filter,
    QPDFObjectHandle const& decode_parms)
{
    auto b = std::make_shared<Buffer>(data.length());
    unsigned char* bp = b->getBuffer();
    if (bp) {
        memcpy(bp, data.c_str(), data.length());
    }
    asStreamWithAssert()->replaceStreamData(b, filter, decode_parms);
}

void
QPDFObjectHandle::replaceStreamData(
    std::shared_ptr<StreamDataProvider> provider,
    QPDFObjectHandle const& filter,
    QPDFObjectHandle const& decode_parms)
{
    asStreamWithAssert()->replaceStreamData(provider, filter, decode_parms);
}

namespace
{
    class FunctionProvider: public QPDFObjectHandle::StreamDataProvider
    {
      public:
        FunctionProvider(std::function<void(Pipeline*)> provider) :
            StreamDataProvider(false),
            p1(provider),
            p2(nullptr)
        {
        }
        FunctionProvider(std::function<bool(Pipeline*, bool, bool)> provider) :
            StreamDataProvider(true),
            p1(nullptr),
            p2(provider)
        {
        }

        virtual void
        provideStreamData(QPDFObjGen const&, Pipeline* pipeline) override
        {
            p1(pipeline);
        }

        virtual bool
        provideStreamData(
            QPDFObjGen const&,
            Pipeline* pipeline,
            bool suppress_warnings,
            bool will_retry) override
        {
            return p2(pipeline, suppress_warnings, will_retry);
        }

      private:
        std::function<void(Pipeline*)> p1;
        std::function<bool(Pipeline*, bool, bool)> p2;
    };
} // namespace

void
QPDFObjectHandle::replaceStreamData(
    std::function<void(Pipeline*)> provider,
    QPDFObjectHandle const& filter,
    QPDFObjectHandle const& decode_parms)
{
    auto sdp =
        std::shared_ptr<StreamDataProvider>(new FunctionProvider(provider));
    asStreamWithAssert()->replaceStreamData(sdp, filter, decode_parms);
}

void
QPDFObjectHandle::replaceStreamData(
    std::function<bool(Pipeline*, bool, bool)> provider,
    QPDFObjectHandle const& filter,
    QPDFObjectHandle const& decode_parms)
{
    auto sdp =
        std::shared_ptr<StreamDataProvider>(new FunctionProvider(provider));
    asStreamWithAssert()->replaceStreamData(sdp, filter, decode_parms);
}

std::map<std::string, QPDFObjectHandle>
QPDFObjectHandle::getPageImages()
{
    return QPDFPageObjectHelper(*this).getImages();
}

std::vector<QPDFObjectHandle>
QPDFObjectHandle::arrayOrStreamToStreamArray(
    std::string const& description, std::string& all_description)
{
    all_description = description;
    std::vector<QPDFObjectHandle> result;
    auto array = asArray();
    if (array) {
        int n_items = array->getNItems();
        for (int i = 0; i < n_items; ++i) {
            QPDFObjectHandle item = array->getItem(i);
            if (item.isStream()) {
                result.push_back(item);
            } else {
                QTC::TC("qpdf", "QPDFObjectHandle non-stream in stream array");
                warn(
                    item.getOwningQPDF(),
                    QPDFExc(
                        qpdf_e_damaged_pdf,
                        "",
                        description + ": item index " + std::to_string(i) +
                            " (from 0)",
                        0,
                        "ignoring non-stream in an array of streams"));
            }
        }
    } else if (isStream()) {
        result.push_back(*this);
    } else if (!isNull()) {
        warn(
            getOwningQPDF(),
            QPDFExc(
                qpdf_e_damaged_pdf,
                "",
                description,
                0,
                " object is supposed to be a stream or an"
                " array of streams but is neither"));
    }

    bool first = true;
    for (auto const& item: result) {
        if (first) {
            first = false;
        } else {
            all_description += ",";
        }
        all_description += " stream " + item.getObjGen().unparse(' ');
    }

    return result;
}

std::vector<QPDFObjectHandle>
QPDFObjectHandle::getPageContents()
{
    std::string description = "page object " + getObjGen().unparse(' ');
    std::string all_description;
    return this->getKey("/Contents")
        .arrayOrStreamToStreamArray(description, all_description);
}

void
QPDFObjectHandle::addPageContents(QPDFObjectHandle new_contents, bool first)
{
    new_contents.assertStream();

    std::vector<QPDFObjectHandle> content_streams;
    if (first) {
        QTC::TC("qpdf", "QPDFObjectHandle prepend page contents");
        content_streams.push_back(new_contents);
    }
    for (auto const& iter: getPageContents()) {
        QTC::TC("qpdf", "QPDFObjectHandle append page contents");
        content_streams.push_back(iter);
    }
    if (!first) {
        content_streams.push_back(new_contents);
    }

    this->replaceKey("/Contents", newArray(content_streams));
}

void
QPDFObjectHandle::rotatePage(int angle, bool relative)
{
    if ((angle % 90) != 0) {
        throw std::runtime_error("QPDF::rotatePage called with an"
                                 " angle that is not a multiple of 90");
    }
    int new_angle = angle;
    if (relative) {
        int old_angle = 0;
        bool found_rotate = false;
        QPDFObjectHandle cur_obj = *this;
        bool searched_parent = false;
        std::set<QPDFObjGen> visited;
        while (!found_rotate) {
            if (visited.count(cur_obj.getObjGen())) {
                // Don't get stuck in an infinite loop
                break;
            }
            if (!visited.empty()) {
                searched_parent = true;
            }
            visited.insert(cur_obj.getObjGen());
            if (cur_obj.getKey("/Rotate").isInteger()) {
                found_rotate = true;
                old_angle = cur_obj.getKey("/Rotate").getIntValueAsInt();
            } else if (cur_obj.getKey("/Parent").isDictionary()) {
                cur_obj = cur_obj.getKey("/Parent");
            } else {
                break;
            }
        }
        QTC::TC(
            "qpdf",
            "QPDFObjectHandle found old angle",
            searched_parent ? 0 : 1);
        if ((old_angle % 90) != 0) {
            old_angle = 0;
        }
        new_angle += old_angle;
    }
    new_angle = (new_angle + 360) % 360;
    // Make this explicit even with new_angle == 0 since /Rotate can
    // be inherited.
    replaceKey("/Rotate", QPDFObjectHandle::newInteger(new_angle));
}

void
QPDFObjectHandle::coalesceContentStreams()
{
    QPDFObjectHandle contents = this->getKey("/Contents");
    if (contents.isStream()) {
        QTC::TC("qpdf", "QPDFObjectHandle coalesce called on stream");
        return;
    } else if (!contents.isArray()) {
        // /Contents is optional for pages, and some very damaged
        // files may have pages that are invalid in other ways.
        return;
    }
    // Should not be possible for a page object to not have an
    // owning PDF unless it was manually constructed in some
    // incorrect way. However, it can happen in a PDF file whose
    // page structure is direct, which is against spec but still
    // possible to hand construct, as in fuzz issue 27393.
    QPDF& qpdf = getQPDF(
        "coalesceContentStreams called on object  with no associated PDF file");

    QPDFObjectHandle new_contents = newStream(&qpdf);
    this->replaceKey("/Contents", new_contents);

    auto provider = std::shared_ptr<StreamDataProvider>(
        new CoalesceProvider(*this, contents));
    new_contents.replaceStreamData(provider, newNull(), newNull());
}

std::string
QPDFObjectHandle::unparse()
{
    std::string result;
    if (this->isIndirect()) {
        result = getObjGen().unparse(' ') + " R";
    } else {
        result = unparseResolved();
    }
    return result;
}

std::string
QPDFObjectHandle::unparseResolved()
{
    if (!dereference()) {
        throw std::logic_error(
            "attempted to dereference an uninitialized QPDFObjectHandle");
    }
    return obj->unparse();
}

std::string
QPDFObjectHandle::unparseBinary()
{
    auto str = asString();
    if (str) {
        return str->unparse(true);
    } else {
        return unparse();
    }
}

// Deprecated versionless getJSON to be removed in qpdf 12
JSON
QPDFObjectHandle::getJSON(bool dereference_indirect)
{
    return getJSON(1, dereference_indirect);
}

JSON
QPDFObjectHandle::getJSON(int json_version, bool dereference_indirect)
{
    if ((!dereference_indirect) && isIndirect()) {
        return JSON::makeString(unparse());
    } else if (!dereference()) {
        throw std::logic_error(
            "attempted to dereference an uninitialized QPDFObjectHandle");
    } else {
        return obj->getJSON(json_version);
    }
}

JSON
QPDFObjectHandle::getStreamJSON(
    int json_version,
    qpdf_json_stream_data_e json_data,
    qpdf_stream_decode_level_e decode_level,
    Pipeline* p,
    std::string const& data_filename)
{
    return asStreamWithAssert()->getStreamJSON(
        json_version, json_data, decode_level, p, data_filename);
}

QPDFObjectHandle
QPDFObjectHandle::wrapInArray()
{
    if (isArray()) {
        return *this;
    }
    QPDFObjectHandle result = QPDFObjectHandle::newArray();
    result.appendItem(*this);
    return result;
}

QPDFObjectHandle
QPDFObjectHandle::parse(
    std::string const& object_str, std::string const& object_description)
{
    return parse(nullptr, object_str, object_description);
}

QPDFObjectHandle
QPDFObjectHandle::parse(
    QPDF* context,
    std::string const& object_str,
    std::string const& object_description)
{
    auto input = std::shared_ptr<InputSource>(
        new BufferInputSource("parsed object", object_str));
    QPDFTokenizer tokenizer;
    bool empty = false;
    QPDFObjectHandle result =
        parse(input, object_description, tokenizer, empty, nullptr, context);
    size_t offset = QIntC::to_size(input->tell());
    while (offset < object_str.length()) {
        if (!isspace(object_str.at(offset))) {
            QTC::TC("qpdf", "QPDFObjectHandle trailing data in parse");
            throw QPDFExc(
                qpdf_e_damaged_pdf,
                input->getName(),
                object_description,
                input->getLastOffset(),
                "trailing data found parsing object from string");
        }
        ++offset;
    }
    return result;
}

void
QPDFObjectHandle::pipePageContents(Pipeline* p)
{
    std::string description = "page object " + getObjGen().unparse(' ');
    std::string all_description;
    this->getKey("/Contents")
        .pipeContentStreams(p, description, all_description);
}

void
QPDFObjectHandle::pipeContentStreams(
    Pipeline* p, std::string const& description, std::string& all_description)
{
    std::vector<QPDFObjectHandle> streams =
        arrayOrStreamToStreamArray(description, all_description);
    bool need_newline = false;
    Pl_Buffer buf("concatenated content stream buffer");
    for (auto stream: streams) {
        if (need_newline) {
            buf.writeCStr("\n");
        }
        LastChar lc(&buf);
        if (!stream.pipeStreamData(&lc, 0, qpdf_dl_specialized)) {
            QTC::TC("qpdf", "QPDFObjectHandle errors in parsecontent");
            throw QPDFExc(
                qpdf_e_damaged_pdf,
                "content stream",
                "content stream object " + stream.getObjGen().unparse(' '),
                0,
                "errors while decoding content stream");
        }
        lc.finish();
        need_newline = (lc.getLastChar() != static_cast<unsigned char>('\n'));
        QTC::TC("qpdf", "QPDFObjectHandle need_newline", need_newline ? 0 : 1);
    }
    std::unique_ptr<Buffer> b(buf.getBuffer());
    p->write(b->getBuffer(), b->getSize());
    p->finish();
}

void
QPDFObjectHandle::parsePageContents(ParserCallbacks* callbacks)
{
    std::string description = "page object " + getObjGen().unparse(' ');
    this->getKey("/Contents")
        .parseContentStream_internal(description, callbacks);
}

void
QPDFObjectHandle::parseAsContents(ParserCallbacks* callbacks)
{
    std::string description = "object " + getObjGen().unparse(' ');
    this->parseContentStream_internal(description, callbacks);
}

void
QPDFObjectHandle::filterPageContents(TokenFilter* filter, Pipeline* next)
{
    auto description =
        "token filter for page object " + getObjGen().unparse(' ');
    Pl_QPDFTokenizer token_pipeline(description.c_str(), filter, next);
    this->pipePageContents(&token_pipeline);
}

void
QPDFObjectHandle::filterAsContents(TokenFilter* filter, Pipeline* next)
{
    auto description = "token filter for object " + getObjGen().unparse(' ');
    Pl_QPDFTokenizer token_pipeline(description.c_str(), filter, next);
    this->pipeStreamData(&token_pipeline, 0, qpdf_dl_specialized);
}

void
QPDFObjectHandle::parseContentStream(
    QPDFObjectHandle stream_or_array, ParserCallbacks* callbacks)
{
    stream_or_array.parseContentStream_internal(
        "content stream objects", callbacks);
}

void
QPDFObjectHandle::parseContentStream_internal(
    std::string const& description, ParserCallbacks* callbacks)
{
    Pl_Buffer buf("concatenated stream data buffer");
    std::string all_description;
    pipeContentStreams(&buf, description, all_description);
    auto stream_data = buf.getBufferSharedPointer();
    callbacks->contentSize(stream_data->getSize());
    try {
        parseContentStream_data(
            stream_data, all_description, callbacks, getOwningQPDF());
    } catch (TerminateParsing&) {
        return;
    }
    callbacks->handleEOF();
}

void
QPDFObjectHandle::parseContentStream_data(
    std::shared_ptr<Buffer> stream_data,
    std::string const& description,
    ParserCallbacks* callbacks,
    QPDF* context)
{
    size_t stream_length = stream_data->getSize();
    auto input = std::shared_ptr<InputSource>(
        new BufferInputSource(description, stream_data.get()));
    QPDFTokenizer tokenizer;
    tokenizer.allowEOF();
    bool empty = false;
    while (QIntC::to_size(input->tell()) < stream_length) {
        // Read a token and seek to the beginning. The offset we get
        // from this process is the beginning of the next
        // non-ignorable (space, comment) token. This way, the offset
        // and don't including ignorable content.
        tokenizer.readToken(input, "content", true);
        qpdf_offset_t offset = input->getLastOffset();
        input->seek(offset, SEEK_SET);
        auto obj = QPDFParser(input, "content", tokenizer, nullptr, context)
                       .parse(empty, true);
        if (!obj.isInitialized()) {
            // EOF
            break;
        }
        size_t length = QIntC::to_size(input->tell() - offset);

        callbacks->handleObject(obj, QIntC::to_size(offset), length);
        if (obj.isOperator() && (obj.getOperatorValue() == "ID")) {
            // Discard next character; it is the space after ID that
            // terminated the token.  Read until end of inline image.
            char ch;
            input->read(&ch, 1);
            tokenizer.expectInlineImage(input);
            QPDFTokenizer::Token t =
                tokenizer.readToken(input, description, true);
            offset = input->getLastOffset();
            length = QIntC::to_size(input->tell() - offset);
            if (t.getType() == QPDFTokenizer::tt_bad) {
                QTC::TC("qpdf", "QPDFObjectHandle EOF in inline image");
                warn(
                    context,
                    QPDFExc(
                        qpdf_e_damaged_pdf,
                        input->getName(),
                        "stream data",
                        input->tell(),
                        "EOF found while reading inline image"));
            } else {
                std::string inline_image = t.getValue();
                QTC::TC("qpdf", "QPDFObjectHandle inline image token");
                callbacks->handleObject(
                    QPDFObjectHandle::newInlineImage(inline_image),
                    QIntC::to_size(offset),
                    length);
            }
        }
    }
}

void
QPDFObjectHandle::addContentTokenFilter(std::shared_ptr<TokenFilter> filter)
{
    coalesceContentStreams();
    this->getKey("/Contents").addTokenFilter(filter);
}

void
QPDFObjectHandle::addTokenFilter(std::shared_ptr<TokenFilter> filter)
{
    return asStreamWithAssert()->addTokenFilter(filter);
}

QPDFObjectHandle
QPDFObjectHandle::parse(
    std::shared_ptr<InputSource> input,
    std::string const& object_description,
    QPDFTokenizer& tokenizer,
    bool& empty,
    StringDecrypter* decrypter,
    QPDF* context)
{
    return QPDFParser(input, object_description, tokenizer, decrypter, context)
        .parse(empty, false);
}

qpdf_offset_t
QPDFObjectHandle::getParsedOffset()
{
    if (dereference()) {
        return this->obj->getParsedOffset();
    } else {
        return -1;
    }
}

QPDFObjectHandle
QPDFObjectHandle::newBool(bool value)
{
    return QPDFObjectHandle(QPDF_Bool::create(value));
}

QPDFObjectHandle
QPDFObjectHandle::newNull()
{
    return QPDFObjectHandle(QPDF_Null::create());
}

QPDFObjectHandle
QPDFObjectHandle::newInteger(long long value)
{
    return QPDFObjectHandle(QPDF_Integer::create(value));
}

QPDFObjectHandle
QPDFObjectHandle::newReal(std::string const& value)
{
    return QPDFObjectHandle(QPDF_Real::create(value));
}

QPDFObjectHandle
QPDFObjectHandle::newReal(
    double value, int decimal_places, bool trim_trailing_zeroes)
{
    return QPDFObjectHandle(
        QPDF_Real::create(value, decimal_places, trim_trailing_zeroes));
}

QPDFObjectHandle
QPDFObjectHandle::newName(std::string const& name)
{
    return QPDFObjectHandle(QPDF_Name::create(name));
}

QPDFObjectHandle
QPDFObjectHandle::newString(std::string const& str)
{
    return QPDFObjectHandle(QPDF_String::create(str));
}

QPDFObjectHandle
QPDFObjectHandle::newUnicodeString(std::string const& utf8_str)
{
    return QPDFObjectHandle(QPDF_String::create_utf16(utf8_str));
}

QPDFObjectHandle
QPDFObjectHandle::newOperator(std::string const& value)
{
    return QPDFObjectHandle(QPDF_Operator::create(value));
}

QPDFObjectHandle
QPDFObjectHandle::newInlineImage(std::string const& value)
{
    return QPDFObjectHandle(QPDF_InlineImage::create(value));
}

QPDFObjectHandle
QPDFObjectHandle::newArray()
{
    return newArray(std::vector<QPDFObjectHandle>());
}

QPDFObjectHandle
QPDFObjectHandle::newArray(std::vector<QPDFObjectHandle> const& items)
{
    return QPDFObjectHandle(QPDF_Array::create(items));
}

QPDFObjectHandle
QPDFObjectHandle::newArray(Rectangle const& rect)
{
    std::vector<QPDFObjectHandle> items;
    items.push_back(newReal(rect.llx));
    items.push_back(newReal(rect.lly));
    items.push_back(newReal(rect.urx));
    items.push_back(newReal(rect.ury));
    return newArray(items);
}

QPDFObjectHandle
QPDFObjectHandle::newArray(Matrix const& matrix)
{
    std::vector<QPDFObjectHandle> items;
    items.push_back(newReal(matrix.a));
    items.push_back(newReal(matrix.b));
    items.push_back(newReal(matrix.c));
    items.push_back(newReal(matrix.d));
    items.push_back(newReal(matrix.e));
    items.push_back(newReal(matrix.f));
    return newArray(items);
}

QPDFObjectHandle
QPDFObjectHandle::newArray(QPDFMatrix const& matrix)
{
    std::vector<QPDFObjectHandle> items;
    items.push_back(newReal(matrix.a));
    items.push_back(newReal(matrix.b));
    items.push_back(newReal(matrix.c));
    items.push_back(newReal(matrix.d));
    items.push_back(newReal(matrix.e));
    items.push_back(newReal(matrix.f));
    return newArray(items);
}

QPDFObjectHandle
QPDFObjectHandle::newFromRectangle(Rectangle const& rect)
{
    return newArray(rect);
}

QPDFObjectHandle
QPDFObjectHandle::newFromMatrix(Matrix const& m)
{
    return newArray(m);
}

QPDFObjectHandle
QPDFObjectHandle::newFromMatrix(QPDFMatrix const& m)
{
    return newArray(m);
}

QPDFObjectHandle
QPDFObjectHandle::newDictionary()
{
    return newDictionary(std::map<std::string, QPDFObjectHandle>());
}

QPDFObjectHandle
QPDFObjectHandle::newDictionary(
    std::map<std::string, QPDFObjectHandle> const& items)
{
    return QPDFObjectHandle(QPDF_Dictionary::create(items));
}

QPDFObjectHandle
QPDFObjectHandle::newStream(QPDF* qpdf)
{
    if (qpdf == nullptr) {
        throw std::runtime_error(
            "attempt to create stream in null qpdf object");
    }
    QTC::TC("qpdf", "QPDFObjectHandle newStream");
    return qpdf->newStream();
}

QPDFObjectHandle
QPDFObjectHandle::newStream(QPDF* qpdf, std::shared_ptr<Buffer> data)
{
    if (qpdf == nullptr) {
        throw std::runtime_error(
            "attempt to create stream in null qpdf object");
    }
    QTC::TC("qpdf", "QPDFObjectHandle newStream with data");
    return qpdf->newStream(data);
}

QPDFObjectHandle
QPDFObjectHandle::newStream(QPDF* qpdf, std::string const& data)
{
    if (qpdf == nullptr) {
        throw std::runtime_error(
            "attempt to create stream in null qpdf object");
    }
    QTC::TC("qpdf", "QPDFObjectHandle newStream with string");
    return qpdf->newStream(data);
}

QPDFObjectHandle
QPDFObjectHandle::newReserved(QPDF* qpdf)
{
    return qpdf->makeIndirectObject(QPDFObjectHandle(QPDF_Reserved::create()));
}

void
QPDFObjectHandle::setObjectDescription(
    QPDF* owning_qpdf, std::string const& object_description)
{
    // This is called during parsing on newly created direct objects,
    // so we can't call dereference() here.
    if (isInitialized() && obj.get()) {
        auto descr =
            std::make_shared<QPDFValue::Description>(object_description);
        obj->setDescription(owning_qpdf, descr);
    }
}

bool
QPDFObjectHandle::hasObjectDescription()
{
    return dereference() && obj.get() && obj->hasDescription();
}

QPDFObjectHandle
QPDFObjectHandle::shallowCopy()
{
    if (!dereference()) {
        throw std::logic_error("operation attempted on uninitialized "
                               "QPDFObjectHandle");
    }
    return QPDFObjectHandle(obj->copy());
}

QPDFObjectHandle
QPDFObjectHandle::unsafeShallowCopy()
{
    if (!dereference()) {
        throw std::logic_error("operation attempted on uninitialized "
                               "QPDFObjectHandle");
    }
    return QPDFObjectHandle(obj->copy(true));
}

void
QPDFObjectHandle::makeDirect(
    std::set<QPDFObjGen>& visited, bool stop_at_streams)
{
    assertInitialized();

    auto cur_og = getObjGen();
    if (cur_og.getObj() != 0) {
        if (visited.count(cur_og)) {
            QTC::TC("qpdf", "QPDFObjectHandle makeDirect loop");
            throw std::runtime_error(
                "loop detected while converting object from "
                "indirect to direct");
        }
        visited.insert(cur_og);
    }

    if (isBool() || isInteger() || isName() || isNull() || isReal() ||
        isString()) {
        this->obj = obj->copy(true);
    } else if (isArray()) {
        std::vector<QPDFObjectHandle> items;
        auto array = asArray();
        int n = array->getNItems();
        for (int i = 0; i < n; ++i) {
            items.push_back(array->getItem(i));
            items.back().makeDirect(visited, stop_at_streams);
        }
        this->obj = QPDF_Array::create(items);
    } else if (isDictionary()) {
        std::map<std::string, QPDFObjectHandle> items;
        auto dict = asDictionary();
        for (auto const& key: getKeys()) {
            items[key] = dict->getKey(key);
            items[key].makeDirect(visited, stop_at_streams);
        }
        this->obj = QPDF_Dictionary::create(items);
    } else if (isStream()) {
        QTC::TC(
            "qpdf", "QPDFObjectHandle copy stream", stop_at_streams ? 0 : 1);
        if (!stop_at_streams) {
            throw std::runtime_error(
                "attempt to make a stream into a direct object");
        }
    } else if (isReserved()) {
        throw std::logic_error("QPDFObjectHandle: attempting to make a"
                               " reserved object handle direct");
    } else {
        throw std::logic_error("QPDFObjectHandle::makeDirectInternal: "
                               "unknown object type");
    }

    if (cur_og.getObj()) {
        visited.erase(cur_og);
    }
}

QPDFObjectHandle
QPDFObjectHandle::copyStream()
{
    assertStream();
    QPDFObjectHandle result = newStream(this->getOwningQPDF());
    QPDFObjectHandle dict = result.getDict();
    QPDFObjectHandle old_dict = getDict();
    for (auto& iter: QPDFDictItems(old_dict)) {
        if (iter.second.isIndirect()) {
            dict.replaceKey(iter.first, iter.second);
        } else {
            dict.replaceKey(iter.first, iter.second.shallowCopy());
        }
    }
    QPDF::StreamCopier::copyStreamData(getOwningQPDF(), result, *this);
    return result;
}

void
QPDFObjectHandle::makeDirect(bool allow_streams)
{
    std::set<QPDFObjGen> visited;
    makeDirect(visited, allow_streams);
}

void
QPDFObjectHandle::assertInitialized() const
{
    if (!isInitialized()) {
        throw std::logic_error("operation attempted on uninitialized "
                               "QPDFObjectHandle");
    }
}

void
QPDFObjectHandle::typeWarning(
    char const* expected_type, std::string const& warning)
{
    QPDF* context = nullptr;
    std::string description;
    // Type checks above guarantee that the object has been dereferenced.
    // Nevertheless, dereference throws exceptions in the test suite
    if (!dereference()) {
        throw std::logic_error(
            "attempted to dereference an uninitialized QPDFObjectHandle");
    }
    this->obj->getDescription(context, description);
    // Null context handled by warn
    warn(
        context,
        QPDFExc(
            qpdf_e_object,
            "",
            description,
            0,
            std::string("operation for ") + expected_type +
                " attempted on object of type " + getTypeName() + ": " +
                warning));
}

void
QPDFObjectHandle::warnIfPossible(std::string const& warning)
{
    QPDF* context = nullptr;
    std::string description;
    if (dereference() && obj->getDescription(context, description)) {
        warn(context, QPDFExc(qpdf_e_damaged_pdf, "", description, 0, warning));
    } else {
        *QPDFLogger::defaultLogger()->getError() << warning << "\n";
    }
}

void
QPDFObjectHandle::objectWarning(std::string const& warning)
{
    QPDF* context = nullptr;
    std::string description;
    // Type checks above guarantee that the object has been dereferenced.
    this->obj->getDescription(context, description);
    // Null context handled by warn
    warn(context, QPDFExc(qpdf_e_object, "", description, 0, warning));
}

void
QPDFObjectHandle::assertType(char const* type_name, bool istype)
{
    if (!istype) {
        throw std::runtime_error(
            std::string("operation for ") + type_name +
            " attempted on object of type " + getTypeName());
    }
}

void
QPDFObjectHandle::assertNull()
{
    assertType("null", isNull());
}

void
QPDFObjectHandle::assertBool()
{
    assertType("boolean", isBool());
}

void
QPDFObjectHandle::assertInteger()
{
    assertType("integer", isInteger());
}

void
QPDFObjectHandle::assertReal()
{
    assertType("real", isReal());
}

void
QPDFObjectHandle::assertName()
{
    assertType("name", isName());
}

void
QPDFObjectHandle::assertString()
{
    assertType("string", isString());
}

void
QPDFObjectHandle::assertOperator()
{
    assertType("operator", isOperator());
}

void
QPDFObjectHandle::assertInlineImage()
{
    assertType("inlineimage", isInlineImage());
}

void
QPDFObjectHandle::assertArray()
{
    assertType("array", isArray());
}

void
QPDFObjectHandle::assertDictionary()
{
    assertType("dictionary", isDictionary());
}

void
QPDFObjectHandle::assertStream()
{
    assertType("stream", isStream());
}

void
QPDFObjectHandle::assertReserved()
{
    assertType("reserved", isReserved());
}

void
QPDFObjectHandle::assertIndirect()
{
    if (!isIndirect()) {
        throw std::logic_error(
            "operation for indirect object attempted on direct object");
    }
}

void
QPDFObjectHandle::assertScalar()
{
    assertType("scalar", isScalar());
}

void
QPDFObjectHandle::assertNumber()
{
    assertType("number", isNumber());
}

bool
QPDFObjectHandle::isPageObject()
{
    // See comments in QPDFObjectHandle.hh.
    if (getOwningQPDF() == nullptr) {
        return false;
    }
    // getAllPages repairs /Type when traversing the page tree.
    getOwningQPDF()->getAllPages();
    if (!this->isDictionary()) {
        return false;
    }
    if (this->hasKey("/Type")) {
        QPDFObjectHandle type = this->getKey("/Type");
        if (type.isNameAndEquals("/Page")) {
            return true;
        }
        // Files have been seen in the wild that have /Type (Page)
        else if (type.isString() && (type.getStringValue() == "Page")) {
            return true;
        } else {
            return false;
        }
    }
    return false;
}

bool
QPDFObjectHandle::isPagesObject()
{
    if (getOwningQPDF() == nullptr) {
        return false;
    }
    // getAllPages repairs /Type when traversing the page tree.
    getOwningQPDF()->getAllPages();
    return isDictionaryOfType("/Pages");
}

bool
QPDFObjectHandle::isFormXObject()
{
    return isStreamOfType("", "/Form");
}

bool
QPDFObjectHandle::isImage(bool exclude_imagemask)
{
    return (
        isStreamOfType("", "/Image") &&
        ((!exclude_imagemask) ||
         (!(getDict().getKey("/ImageMask").isBool() &&
            getDict().getKey("/ImageMask").getBoolValue()))));
}

void
QPDFObjectHandle::checkOwnership(QPDFObjectHandle const& item) const
{
    auto qpdf = getOwningQPDF();
    auto item_qpdf = item.getOwningQPDF();
    if ((qpdf != nullptr) && (item_qpdf != nullptr) && (qpdf != item_qpdf)) {
        QTC::TC("qpdf", "QPDFObjectHandle check ownership");
        throw std::logic_error(
            "Attempting to add an object from a different QPDF."
            " Use QPDF::copyForeignObject to add objects from another file.");
    }
}

void
QPDFObjectHandle::assertPageObject()
{
    if (!isPageObject()) {
        throw std::runtime_error("page operation called on non-Page object");
    }
}

inline bool
QPDFObjectHandle::dereference()
{
    if (!isInitialized()) {
        return false;
    }
    this->obj->resolve();
    return true;
}

void
QPDFObjectHandle::warn(QPDF* qpdf, QPDFExc const& e)
{
    // If parsing on behalf of a QPDF object and want to give a
    // warning, we can warn through the object. If parsing for some
    // other reason, such as an explicit creation of an object from a
    // string, then just throw the exception.
    if (qpdf) {
        qpdf->warn(e);
    } else {
        throw e;
    }
}

QPDFObjectHandle::QPDFDictItems::QPDFDictItems(QPDFObjectHandle const& oh) :
    oh(oh)
{
}

QPDFObjectHandle::QPDFDictItems::iterator&
QPDFObjectHandle::QPDFDictItems::iterator::operator++()
{
    ++this->m->iter;
    updateIValue();
    return *this;
}

QPDFObjectHandle::QPDFDictItems::iterator&
QPDFObjectHandle::QPDFDictItems::iterator::operator--()
{
    --this->m->iter;
    updateIValue();
    return *this;
}

QPDFObjectHandle::QPDFDictItems::iterator::reference
QPDFObjectHandle::QPDFDictItems::iterator::operator*()
{
    updateIValue();
    return this->ivalue;
}

QPDFObjectHandle::QPDFDictItems::iterator::pointer
QPDFObjectHandle::QPDFDictItems::iterator::operator->()
{
    updateIValue();
    return &this->ivalue;
}

bool
QPDFObjectHandle::QPDFDictItems::iterator::operator==(
    iterator const& other) const
{
    if (this->m->is_end && other.m->is_end) {
        return true;
    }
    if (this->m->is_end || other.m->is_end) {
        return false;
    }
    return (this->ivalue.first == other.ivalue.first);
}

QPDFObjectHandle::QPDFDictItems::iterator::iterator(
    QPDFObjectHandle& oh, bool for_begin) :
    m(new Members(oh, for_begin))
{
    updateIValue();
}

void
QPDFObjectHandle::QPDFDictItems::iterator::updateIValue()
{
    this->m->is_end = (this->m->iter == this->m->keys.end());
    if (this->m->is_end) {
        this->ivalue.first = "";
        this->ivalue.second = QPDFObjectHandle();
    } else {
        this->ivalue.first = *(this->m->iter);
        this->ivalue.second = this->m->oh.getKey(this->ivalue.first);
    }
}

QPDFObjectHandle::QPDFDictItems::iterator::Members::Members(
    QPDFObjectHandle& oh, bool for_begin) :
    oh(oh)
{
    this->keys = oh.getKeys();
    this->iter = for_begin ? this->keys.begin() : this->keys.end();
}

QPDFObjectHandle::QPDFDictItems::iterator
QPDFObjectHandle::QPDFDictItems::begin()
{
    return iterator(oh, true);
}

QPDFObjectHandle::QPDFDictItems::iterator
QPDFObjectHandle::QPDFDictItems::end()
{
    return iterator(oh, false);
}

QPDFObjectHandle::QPDFArrayItems::QPDFArrayItems(QPDFObjectHandle const& oh) :
    oh(oh)
{
}

QPDFObjectHandle::QPDFArrayItems::iterator&
QPDFObjectHandle::QPDFArrayItems::iterator::operator++()
{
    if (!this->m->is_end) {
        ++this->m->item_number;
        updateIValue();
    }
    return *this;
}

QPDFObjectHandle::QPDFArrayItems::iterator&
QPDFObjectHandle::QPDFArrayItems::iterator::operator--()
{
    if (this->m->item_number > 0) {
        --this->m->item_number;
        updateIValue();
    }
    return *this;
}

QPDFObjectHandle::QPDFArrayItems::iterator::reference
QPDFObjectHandle::QPDFArrayItems::iterator::operator*()
{
    updateIValue();
    return this->ivalue;
}

QPDFObjectHandle::QPDFArrayItems::iterator::pointer
QPDFObjectHandle::QPDFArrayItems::iterator::operator->()
{
    updateIValue();
    return &this->ivalue;
}

bool
QPDFObjectHandle::QPDFArrayItems::iterator::operator==(
    iterator const& other) const
{
    return (this->m->item_number == other.m->item_number);
}

QPDFObjectHandle::QPDFArrayItems::iterator::iterator(
    QPDFObjectHandle& oh, bool for_begin) :
    m(new Members(oh, for_begin))
{
    updateIValue();
}

void
QPDFObjectHandle::QPDFArrayItems::iterator::updateIValue()
{
    this->m->is_end = (this->m->item_number >= this->m->oh.getArrayNItems());
    if (this->m->is_end) {
        this->ivalue = QPDFObjectHandle();
    } else {
        this->ivalue = this->m->oh.getArrayItem(this->m->item_number);
    }
}

QPDFObjectHandle::QPDFArrayItems::iterator::Members::Members(
    QPDFObjectHandle& oh, bool for_begin) :
    oh(oh)
{
    this->item_number = for_begin ? 0 : oh.getArrayNItems();
}

QPDFObjectHandle::QPDFArrayItems::iterator
QPDFObjectHandle::QPDFArrayItems::begin()
{
    return iterator(oh, true);
}

QPDFObjectHandle::QPDFArrayItems::iterator
QPDFObjectHandle::QPDFArrayItems::end()
{
    return iterator(oh, false);
}

QPDFObjGen
QPDFObjectHandle::getObjGen() const
{
    return isInitialized() ? obj->getObjGen() : QPDFObjGen();
}

// Indirect object accessors
QPDF*
QPDFObjectHandle::getOwningQPDF() const
{
    return isInitialized() ? this->obj->getQPDF() : nullptr;
}

QPDF&
QPDFObjectHandle::getQPDF(std::string const& error_msg) const
{
    auto result = isInitialized() ? this->obj->getQPDF() : nullptr;
    if (result == nullptr) {
        throw std::runtime_error(
            error_msg == "" ? "attempt to use a null qpdf object" : error_msg);
    }
    return *result;
}

void
QPDFObjectHandle::setParsedOffset(qpdf_offset_t offset)
{
    // This is called during parsing on newly created direct objects,
    // so we can't call dereference() here.
    if (isInitialized()) {
        this->obj->setParsedOffset(offset);
    }
}

QPDFObjectHandle
operator""_qpdf(char const* v, size_t len)
{
    return QPDFObjectHandle::parse(
        std::string(v, len), "QPDFObjectHandle literal");
}
