// SPDX-FileCopyrightText: 2022 James R. Barlow
// SPDX-License-Identifier: MPL-2.0

#include <cerrno>
#include <cstring>
#include <sstream>
#include <type_traits>

#include "pikepdf.h"
#include "qpdf_lock.h"

#include <qpdf/Buffer.hh>
#include <qpdf/BufferInputSource.hh>
#include <qpdf/Pl_Discard.hh>
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
#include <qpdf/QPDFEmbeddedFileDocumentHelper.hh>
#include <qpdf/QPDFExc.hh>
#include <qpdf/QPDFLogger.hh>
#include <qpdf/QPDFObjGen.hh>
#include <qpdf/QPDFPageDocumentHelper.hh>
#include <qpdf/QPDFSystemError.hh>
#include <qpdf/QPDFWriter.hh>
#include <qpdf/QPDFXRefEntry.hh>

#include "jbig2-inl.h"
#include "mmap_inputsource-inl.h"
#include "pipeline.h"
#include "qpdf_inputsource-inl.h"
#include "qpdf_pagelist.h"
#include "utils.h"

enum access_mode_e { access_default, access_stream, access_mmap, access_mmap_only };

// Create a shared_ptr<QPDF> with a custom deleter that unregisters from the
// per-QPDF mutex registry. This ensures cleanup even if the user never
// explicitly calls close().
std::shared_ptr<QPDF> make_registered_qpdf()
{
    auto q = std::shared_ptr<QPDF>(new QPDF(), [](QPDF *p) {
        QpdfRegistry::instance().unregister_qpdf(p);
        delete p;
    });
    QpdfRegistry::instance().register_qpdf(q.get());
    return q;
}

void qpdf_basic_settings(QPDF &q) // LCOV_EXCL_LINE
{
    q.setSuppressWarnings(true);
    q.setImmediateCopyFrom(true);
    q.setLogger(get_pikepdf_logger());
}

// The holder for QPDF should still be shared_ptr because the Python Pdf.pages accessor
// needs to keep a reference to the QPDF, since users can do things like:
//  accessor = pdf.pages
//  del pdf
//  accessor[0]
std::shared_ptr<QPDF> open_pdf(py::object stream,
    py::object password_arg,
    bool hex_password = false,
    bool ignore_xref_streams = false,
    bool suppress_warnings = true,
    bool attempt_recovery = true,
    bool inherit_page_attributes = true,
    access_mode_e access_mode = access_mode_e::access_default,
    std::string description = "",
    bool closing_stream = false)
{
    std::string password = to_string(password_arg);
    auto q = make_registered_qpdf();

    qpdf_basic_settings(*q);
    q->setSuppressWarnings(suppress_warnings);
    q->setPasswordIsHexKey(hex_password);
    q->setIgnoreXRefStreams(ignore_xref_streams);
    q->setAttemptRecovery(attempt_recovery);

    bool success = false;
    if (access_mode == access_default)
        access_mode = get_mmap_default() ? access_mmap : access_stream;

    if (access_mode == access_mmap || access_mode == access_mmap_only) {
        try {
            auto mmap_input_source =
                std::make_unique<MmapInputSource>(stream, description, closing_stream);
            auto input_source =
                std::shared_ptr<InputSource>(mmap_input_source.release());
            py::gil_scoped_release release;
            q->processInputSource(input_source, password.c_str());
            success = true;
        } catch (const py::python_error &) {
            if (access_mode == access_mmap) {
                // Prepare to fallback to stream access
                stream.attr("seek")(0);
                access_mode = access_stream;
            } else {
                throw;
            }
        }
    }

    if (!success && access_mode == access_stream) {
        auto stream_input_source = std::make_unique<PythonStreamInputSource>(
            stream, description, closing_stream);
        auto input_source = std::shared_ptr<InputSource>(stream_input_source.release());
        py::gil_scoped_release release;
        q->processInputSource(input_source, password.c_str());
        success = true;
    }

    if (!success) {
        // LCOV_EXCL_START
        throw std::logic_error(
            "open_pdf: should have succeeded or thrown a Python exception");
        // LCOV_EXCL_STOP
    }

    if (inherit_page_attributes) {
        // This could be expensive for a large file, plausibly (not tested),
        // so release the GIL again.
        py::gil_scoped_release release;
        q->pushInheritedAttributesToPage();
    }

    if (!password.empty() && !q->isEncrypted()) {
        python_warning(
            "A password was provided, but no password was needed to open this PDF.");
    }

    return q;
}

class PikeProgressReporter : public QPDFWriter::ProgressReporter {
public:
    PikeProgressReporter(py::object callback) { this->callback = callback; }

    virtual ~PikeProgressReporter() = default;

    virtual void reportProgress(int percent) override
    {
        py::gil_scoped_acquire acquire;
        this->callback(percent);
    }

private:
    py::object callback;
};

void update_xmp_pdfversion(QPDF &q, std::string version)
{
    auto impl =
        py::module_::import_("pikepdf._cpphelpers").attr("update_xmp_pdfversion");
    auto pypdf = py::cast(&q, py::rv_policy::reference);
    impl(pypdf, version);
}

std::string encryption_password(
    py::dict encryption, const int encryption_level, const char *keyname)
{
    std::string result;
    if (encryption.contains(keyname)) {
        if (encryption[keyname].is_none())
            throw py::value_error((std::string("Encryption ") + keyname +
                                   " may not be None; use empty string?")
                    .c_str());
        if (encryption_level <= 4) {
            auto success =
                QUtil::utf8_to_pdf_doc(to_string(encryption[keyname]), result);
            if (!success)
                throw py::value_error("Encryption level is R3/R4 and password is not "
                                      "encodable as PDFDocEncoding");
        } else {
            result = to_string(encryption[keyname]);
        }
    }
    return result;
}

void setup_encryption(QPDFWriter &w, py::object encryption_obj)
{
    std::string owner;
    std::string user;

    py::dict encryption;

    bool aes = true;
    bool metadata = true;
    std::map<std::string, bool> allow;
    int encryption_level = 6;

    if (py::isinstance<py::tuple>(encryption_obj)) {
        encryption = py::borrow<py::dict>(encryption_obj.attr("_asdict")());
    } else {
        encryption = py::borrow<py::dict>(encryption_obj);
    }

    if (encryption.contains("R")) {
        if (!py::isinstance<py::int_>(encryption["R"]))
            throw py::type_error("Encryption level 'R' must be an integer");
        encryption_level = py::cast<int>(encryption["R"]);
    }
    if (encryption_level < 2 || encryption_level > 6)
        throw py::value_error("Invalid encryption level: must be 2, 3, 4 or 6");

    if (encryption_level == 5) {
        python_warning("Encryption R=5 is deprecated");
    }

    owner = encryption_password(encryption, encryption_level, "owner");
    user = encryption_password(encryption, encryption_level, "user");

    if (encryption.contains("allow")) {
        auto pyallow = encryption["allow"];
        allow["accessibility"] = py::cast<bool>(pyallow.attr("accessibility"));
        allow["extract"] = py::cast<bool>(pyallow.attr("extract"));
        allow["modify_assembly"] = py::cast<bool>(pyallow.attr("modify_assembly"));
        allow["modify_annotation"] = py::cast<bool>(pyallow.attr("modify_annotation"));
        allow["modify_form"] = py::cast<bool>(pyallow.attr("modify_form"));
        allow["modify_other"] = py::cast<bool>(pyallow.attr("modify_other"));
        allow["print_lowres"] = py::cast<bool>(pyallow.attr("print_lowres"));
        allow["print_highres"] = py::cast<bool>(pyallow.attr("print_highres"));
    }
    if (encryption.contains("aes")) {
        if (py::isinstance<py::bool_>(encryption["aes"]))
            aes = py::cast<bool>(encryption["aes"]);
        else
            throw py::type_error("aes must be bool");
    } else {
        aes = (encryption_level >= 4);
    }
    if (encryption.contains("metadata")) {
        if (py::isinstance<py::bool_>(encryption["metadata"]))
            metadata = py::cast<bool>(encryption["metadata"]);
        else
            throw py::type_error("metadata must be bool");
    } else {
        metadata = (encryption_level >= 4);
    }

    if (metadata && encryption_level < 4) {
        throw py::value_error("Cannot encrypt metadata when R < 4");
    }
    if (aes && encryption_level < 4) {
        throw py::value_error("Cannot encrypt with AES when R < 4");
    }
    if (encryption_level == 6 && !aes) {
        throw py::value_error("When R = 6, AES encryption must be enabled");
    }
    if (metadata && !aes) {
        throw py::value_error(
            "Cannot encrypt metadata unless AES encryption is enabled");
    }

    qpdf_r3_print_e print;
    if (allow["print_highres"])
        print = qpdf_r3p_full;
    else if (allow["print_lowres"])
        print = qpdf_r3p_low;
    else
        print = qpdf_r3p_none;

    if (encryption_level == 6) {
        w.setR6EncryptionParameters(user.c_str(),
            owner.c_str(),
            allow["accessibility"],
            allow["extract"],
            allow["modify_assembly"],
            allow["modify_annotation"],
            allow["modify_form"],
            allow["modify_other"],
            print,
            metadata);
    } else if (encryption_level == 5) {
        w.setR5EncryptionParameters(user.c_str(),
            owner.c_str(),
            allow["accessibility"],
            allow["extract"],
            allow["modify_assembly"],
            allow["modify_annotation"],
            allow["modify_form"],
            allow["modify_other"],
            print,
            metadata);
    } else if (encryption_level == 4) {
        w.setR4EncryptionParametersInsecure(user.c_str(),
            owner.c_str(),
            allow["accessibility"],
            allow["extract"],
            allow["modify_assembly"],
            allow["modify_annotation"],
            allow["modify_form"],
            allow["modify_other"],
            print,
            metadata,
            aes);
    } else if (encryption_level == 3) {
        w.setR3EncryptionParametersInsecure(user.c_str(),
            owner.c_str(),
            allow["accessibility"],
            allow["extract"],
            allow["modify_assembly"],
            allow["modify_annotation"],
            allow["modify_form"],
            allow["modify_other"],
            print);
    } else if (encryption_level == 2) {
        w.setR2EncryptionParametersInsecure(user.c_str(),
            owner.c_str(),
            (print != qpdf_r3p_none),
            allow["modify_assembly"],
            allow["extract"],
            allow["modify_annotation"]);
    }
}

typedef std::pair<std::string, int> pdf_version_extension;

pdf_version_extension get_version_extension(py::object ver_ext)
{
    std::string version = "";
    int extension = 0;
    try {
        version = py::cast<std::string>(ver_ext);
        extension = 0;
    } catch (const py::cast_error &) {
        try {
            auto version_ext = py::cast<pdf_version_extension>(ver_ext);
            version = version_ext.first;
            extension = version_ext.second;
        } catch (const py::cast_error &) {
            throw py::type_error("PDF version must be a tuple: (str, int)");
        }
    }
    return pdf_version_extension(version, extension);
}

void save_pdf(QPDF &q,
    py::object stream,
    bool static_id = false,
    bool preserve_pdfa = true,
    py::object min_version = py::none(),
    py::object force_version = py::none(),
    bool fix_metadata_version = true,
    bool compress_streams = true,
    py::object stream_decode_level = py::none(),
    qpdf_object_stream_e object_stream_mode = qpdf_o_preserve,
    bool normalize_content = false,
    bool linearize = false,
    bool qdf = false,
    py::object progress = py::none(),
    py::object encryption = py::none(),
    bool samefile_check = true,
    bool recompress_flate = false,
    bool deterministic_id = false)
{
    QpdfLockGuard lock(&q);
    QPDFWriter w(q);

    if (static_id) {
        w.setStaticID(true);
    }
    if (deterministic_id) {
        w.setDeterministicID(true);
    }
    w.setNewlineBeforeEndstream(preserve_pdfa);

    if (!min_version.is_none()) {
        auto version_ext = get_version_extension(min_version);
        w.setMinimumPDFVersion(version_ext.first, version_ext.second);
    }
    w.setCompressStreams(compress_streams);
    if (!stream_decode_level.is_none()) {
        // Unconditionally calling setDecodeLevel has side effects, disabling
        // preserve encryption in particular
        w.setDecodeLevel(py::cast<qpdf_stream_decode_level_e>(stream_decode_level));
    }
    w.setObjectStreamMode(object_stream_mode);
    w.setRecompressFlate(recompress_flate);

    std::string description = py::cast<std::string>(py::repr(stream));

    // We must set up the output pipeline before we configure encryption
    Pl_PythonOutput output_pipe(description.c_str(), stream);
    w.setOutputPipeline(&output_pipe);

    // Possibilities:
    // encryption=True -> preserve existing
    // encryption=False -> remove encryption
    // encryption=None -> remove encryption
    // encryption=<dict or NamedTuple> apply new encryption settings

    bool removing_encryption =
        encryption.is_none() || encryption.equal(py::bool_(false));
    if (!removing_encryption) {
        if ((normalize_content || !stream_decode_level.is_none())) {
            throw py::value_error("cannot save with encryption and normalize_content "
                                  "or stream_decode_level");
        }
    }
    bool preserving_encryption = encryption.equal(py::bool_(true));

    if (preserving_encryption) {
        if (!q.isEncrypted()) {
            throw py::value_error("can't preserve encryption parameters on "
                                  "a file with no encryption");
        }
        w.setPreserveEncryption(true); // Keep existing encryption
    } else if (removing_encryption) {
        // Note: encryption.equal(py::bool_(false)) != !preserving_encryption
        w.setPreserveEncryption(false); // Remove encryption
    } else {
        setup_encryption(w, encryption);
    }

    if (normalize_content && linearize) {
        throw py::value_error("cannot save with both normalize_content and linearize");
    }
    w.setContentNormalization(normalize_content);
    w.setLinearization(linearize);
    w.setQDFMode(qdf);

    if (!force_version.is_none()) {
        auto version_ext = get_version_extension(force_version);
        w.forcePDFVersion(version_ext.first, version_ext.second);
    }
    if (fix_metadata_version) {
        update_xmp_pdfversion(q, w.getFinalVersion());
    }

    if (!progress.is_none()) {
        std::shared_ptr<QPDFWriter::ProgressReporter> reporter =
            std::make_shared<PikeProgressReporter>(progress);
        w.registerProgressReporter(reporter);
    }

    w.write();
}

void init_qpdf(py::module_ &m)
{
    QPDF::registerStreamFilter("/JBIG2Decode", &JBIG2StreamFilter::factory);

    py::enum_<qpdf_object_stream_e>(m, "ObjectStreamMode")
        .value("disable", qpdf_object_stream_e::qpdf_o_disable)
        .value("preserve", qpdf_object_stream_e::qpdf_o_preserve)
        .value("generate", qpdf_object_stream_e::qpdf_o_generate);

    py::enum_<qpdf_stream_decode_level_e>(m, "StreamDecodeLevel")
        .value("none", qpdf_stream_decode_level_e::qpdf_dl_none)
        .value("generalized", qpdf_stream_decode_level_e::qpdf_dl_generalized)
        .value("specialized", qpdf_stream_decode_level_e::qpdf_dl_specialized)
        .value("all", qpdf_stream_decode_level_e::qpdf_dl_all);

    py::enum_<QPDF::encryption_method_e>(m, "EncryptionMethod")
        .value("none", QPDF::encryption_method_e::e_none)
        .value("unknown", QPDF::encryption_method_e::e_unknown)
        .value("rc4", QPDF::encryption_method_e::e_rc4)
        .value("aes", QPDF::encryption_method_e::e_aes)
        .value("aesv3", QPDF::encryption_method_e::e_aesv3);

    py::enum_<access_mode_e>(m, "AccessMode")
        .value("default", access_mode_e::access_default)
        .value("stream", access_mode_e::access_stream)
        .value("mmap", access_mode_e::access_mmap)
        .value("mmap_only", access_mode_e::access_mmap_only);

    py::class_<QPDF>(m, "Pdf", "In-memory representation of a PDF", py::dynamic_attr())
        .def_static("new",
            []() {
                auto q = make_registered_qpdf();
                q->emptyPDF();
                qpdf_basic_settings(*q);
                return q;
            })
        .def_static("_open",
            open_pdf,
            py::arg("stream"),
            py::kw_only(),
            py::arg("password") = py::str(""),
            py::arg("hex_password") = false,
            py::arg("ignore_xref_streams") = false,
            py::arg("suppress_warnings") = true,
            py::arg("attempt_recovery") = true,
            py::arg("inherit_page_attributes") = true,
            py::arg("access_mode") = access_mode_e::access_default,
            py::arg("description") = "",
            py::arg("closing_stream") = false)
        .def("__repr__",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return std::string("<pikepdf.Pdf description='") + q.getFilename() +
                       std::string("'>");
            })
        .def_prop_ro("filename",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getFilename();
            })
        .def_prop_ro("pdf_version",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getPDFVersion();
            })
        .def_prop_ro("extension_level",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getExtensionLevel();
            })
        .def_prop_ro("Root",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getRoot();
            })
        .def_prop_ro("trailer",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getTrailer();
            })
        .def_prop_ro(
            "pages",
            [](std::shared_ptr<QPDF> q) {
                QpdfLockGuard lock(q.get());
                return PageList(q);
            },
            py::rv_policy::reference_internal)
        .def_prop_ro("_pages",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getAllPages();
            })
        .def_prop_ro("is_encrypted",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.isEncrypted();
            })
        .def_prop_ro("is_linearized",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.isLinearized();
            })
        .def(
            "check_linearization",
            [](QPDF &q, py::object stream) {
                QpdfLockGuard lock(&q);
                auto sys = py::module_::import_("sys");
                if (stream.is_none())
                    stream = sys.attr("stderr");
                auto old_stderr = sys.attr("stderr");
                sys.attr("stderr") = stream;
                try {
                    bool result = q.checkLinearization();
                    sys.attr("stderr") = old_stderr;
                    return result;
                } catch (...) {
                    sys.attr("stderr") = old_stderr;
                    throw;
                }
            },
            py::arg("stream") = py::none())
        .def("get_warnings", // this is a def because it modifies state by clearing
                             // warnings
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                py::list warnings;
                for (auto w : q.getWarnings()) {
                    auto what_str = std::string(w.what());
                    py::bytes msg_bytes(what_str.data(), what_str.size());
                    warnings.append(msg_bytes.attr("decode")("utf-8", "replace"));
                }
                return warnings;
            })
        .def("show_xref_table",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                q.showXRefTable();
            })
        .def(
            "_add_page",
            [](QPDF &q, QPDFObjectHandle &page, bool first = false) {
                QpdfLockGuard lock(&q);
                q.addPage(page, first);
            },
            py::arg("page"),
            py::arg("first") = false)
        .def("_remove_page",
            [](QPDF &q, QPDFObjectHandle &page) {
                QpdfLockGuard lock(&q);
                q.removePage(page);
            })
        .def("remove_unreferenced_resources",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                QPDFPageDocumentHelper helper(q);
                helper.removeUnreferencedResources();
            })
        .def("_save",
            save_pdf,
            py::arg("stream"),
            py::kw_only(),
            py::arg("static_id") = false,
            py::arg("preserve_pdfa") = true,
            py::arg("min_version").none() = py::none(),
            py::arg("force_version").none() = py::none(),
            py::arg("fix_metadata_version") = true,
            py::arg("compress_streams") = true,
            py::arg("stream_decode_level").none() = py::none(),
            py::arg("object_stream_mode") = qpdf_object_stream_e::qpdf_o_preserve,
            py::arg("normalize_content") = false,
            py::arg("linearize") = false,
            py::arg("qdf") = false,
            py::arg("progress").none() = py::none(),
            py::arg("encryption").none() = py::none(),
            py::arg("samefile_check") = true,
            py::arg("recompress_flate") = false,
            py::arg("deterministic_id") = false)
        .def("_get_object_id",
            [](QPDF &q, int objid, int gen) {
                QpdfLockGuard lock(&q);
                return q.getObjectByID(objid, gen);
            })
        .def(
            "get_object",
            [](QPDF &q, std::pair<int, int> objgen) {
                QpdfLockGuard lock(&q);
                return q.getObjectByID(objgen.first, objgen.second);
            },
            py::arg("objgen"))
        .def(
            "get_object",
            [](QPDF &q, int objid, int gen) {
                QpdfLockGuard lock(&q);
                return q.getObjectByID(objid, gen);
            },
            py::arg("objid"),
            py::arg("gen"))
        .def_prop_ro(
            "objects",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.getAllObjects();
            },
            py::rv_policy::reference_internal)
        .def(
            "make_indirect",
            [](QPDF &q, QPDFObjectHandle &h) {
                QpdfLockGuard lock(&q);
                return q.makeIndirectObject(h);
            },
            py::arg("h"))
        .def(
            "make_indirect",
            [](QPDF &q, py::object obj) -> QPDFObjectHandle {
                QpdfLockGuard lock(&q);
                return q.makeIndirectObject(objecthandle_encode(obj));
            },
            py::arg("obj"))
        .def(
            "copy_foreign",
            [](QPDF &q, QPDFObjectHandle &h) -> QPDFObjectHandle {
                DualQpdfLockGuard lock(&q, h.getOwningQPDF());
                return q.copyForeignObject(h);
            },
            py::arg("h"))
        .def("copy_foreign",
            [](QPDF &q, QPDFPageObjectHelper &poh) -> QPDFPageObjectHelper {
                PyErr_SetString(PyExc_NotImplementedError,
                    "Use pikepdf.Pdf.pages interface to copy "
                    "pages from one PDF to another.");
                throw py::python_error();
            })
        .def("_replace_object",
            [](QPDF &q, std::pair<int, int> objgen, QPDFObjectHandle &h) {
                QpdfLockGuard lock(&q);
                q.replaceObject(objgen.first, objgen.second, h);
            })
        .def("_swap_objects",
            [](QPDF &q, std::pair<int, int> objgen1, std::pair<int, int> objgen2) {
                QpdfLockGuard lock(&q);
                QPDFObjGen o1(objgen1.first, objgen1.second);
                QPDFObjGen o2(objgen2.first, objgen2.second);
                q.swapObjects(o1, o2);
            })
        .def(
            "_close",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                q.closeInputSource();
            },
            "Used to implement Pdf.close().")
        .def(
            "_decode_all_streams_and_discard",
            [](QPDF &q, py::object progress) {
                QpdfLockGuard lock(&q);
                QPDFWriter w(q);
                Pl_Discard discard;
                w.setOutputPipeline(&discard);
                w.setDecodeLevel(qpdf_dl_all);
                w.setLinearization(false);
                w.setCompressStreams(false);
                w.setRecompressFlate(false);
                if (!progress.is_none()) {
                    w.registerProgressReporter(
                        std::make_shared<PikeProgressReporter>(progress));
                }
                try {
                    w.write();
                } catch (py::python_error &e) {
                    auto cls_dependency_error =
                        py::module_::import_("pikepdf._exceptions")
                            .attr("DependencyError");
                    if (e.matches(cls_dependency_error)) {
                        python_warning(
                            "pikepdf is missing some specialized decoders "
                            "(probably jbig2dec) so not all stream contents can be "
                            "tested.");
                        w.setDecodeLevel(qpdf_dl_generalized);
                        w.write();
                    } else {
                        throw;
                    }
                }
            },
            py::arg("progress").none() = py::none())
        .def_prop_ro("_allow_accessibility",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowAccessibility();
            })
        .def_prop_ro("_allow_extract",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowExtractAll();
            })
        .def_prop_ro("_allow_print_lowres",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowPrintLowRes();
            })
        .def_prop_ro("_allow_print_highres",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowPrintHighRes();
            })
        .def_prop_ro("_allow_modify_assembly",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowModifyAssembly();
            })
        .def_prop_ro("_allow_modify_form",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowModifyForm();
            })
        .def_prop_ro("_allow_modify_annotation",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowModifyAnnotation();
            })
        .def_prop_ro("_allow_modify_other",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowModifyOther();
            })
        .def_prop_ro("_allow_modify_all",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.allowModifyAll();
            })
        .def_prop_ro("_encryption_data",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                int R = 0;
                int P = 0;
                int V = 0;
                QPDF::encryption_method_e stream_method = QPDF::e_unknown;
                QPDF::encryption_method_e string_method = QPDF::e_unknown;
                QPDF::encryption_method_e file_method = QPDF::e_unknown;
                if (!q.isEncrypted(R, P, V, stream_method, string_method, file_method))
                    return py::dict();

                auto user_passwd = q.getTrimmedUserPassword();
                auto encryption_key = q.getEncryptionKey();

                py::dict result;
                result["R"] = py::cast(R);
                result["P"] = py::cast(P);
                result["V"] = py::cast(V);
                result["stream"] = py::cast(stream_method);
                result["string"] = py::cast(string_method);
                result["file"] = py::cast(file_method);
                result["user_passwd"] =
                    py::bytes(user_passwd.data(), user_passwd.size());
                result["encryption_key"] =
                    py::bytes(encryption_key.data(), encryption_key.size());
                return result;
            })
        .def_prop_ro("user_password_matched",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.userPasswordMatched();
            })
        .def_prop_ro("owner_password_matched",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return q.ownerPasswordMatched();
            })
        .def("generate_appearance_streams",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                QPDFAcroFormDocumentHelper afdh(q);
                afdh.generateAppearancesIfNeeded();
            })
        .def(
            "flatten_annotations",
            [](QPDF &q, std::string mode) {
                QpdfLockGuard lock(&q);
                QPDFPageDocumentHelper dh(q);
                auto required = 0;
                auto forbidden = an_invisible | an_hidden;

                if (mode == "screen") {
                    forbidden |= an_no_view;
                } else if (mode == "print") {
                    required |= an_print;
                } else if (mode == "" || mode == "all") {
                    // No op
                } else {
                    throw py::value_error(
                        "Mode must be one of 'all', 'screen', 'print'.");
                }

                dh.flattenAnnotations(required, forbidden);
            },
            py::arg("mode") = "all") // class Pdf
        .def_prop_ro("acroform",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return QPDFAcroFormDocumentHelper(q);
            })
        .def_prop_ro("attachments",
            [](QPDF &q) {
                QpdfLockGuard lock(&q);
                return QPDFEmbeddedFileDocumentHelper(q);
            })
        .def("_acquire_lock",
            [](QPDF &q) {
                auto *m = QpdfRegistry::instance().lookup(&q);
                if (m)
                    m->lock();
            })
        .def("_release_lock", [](QPDF &q) {
            auto *m = QpdfRegistry::instance().lookup(&q);
            if (m)
                m->unlock();
        });
}
