/* poppler-document.cc: glib wrapper for poppler
 * Copyright (C) 2005, Red Hat, Inc.
 *
 * Copyright (C) 2016 Jakub Alba <jakubalba@gmail.com>
 * Copyright (C) 2018, 2019, 2021, 2022 Marek Kasik <mkasik@redhat.com>
 * Copyright (C) 2019 Masamichi Hosoda <trueroad@trueroad.jp>
 * Copyright (C) 2019, 2021 Oliver Sander <oliver.sander@tu-dresden.de>
 * Copyright (C) 2020, 2022 Albert Astals Cid <aacid@kde.org>
 * Copyright (C) 2021 André Guerreiro <aguerreiro1985@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <cstring>

#include <glib.h>

#ifndef G_OS_WIN32
#    include <fcntl.h>
#    include <sys/stat.h>
#    include <sys/types.h>
#    include <unistd.h>
#endif

#ifndef __GI_SCANNER__
#    include <memory>

#    include <goo/gfile.h>
#    include <splash/SplashBitmap.h>
#    include <CachedFile.h>
#    include <DateInfo.h>
#    include <FILECacheLoader.h>
#    include <GlobalParams.h>
#    include <PDFDoc.h>
#    include <SignatureInfo.h>
#    include <Outline.h>
#    include <ErrorCodes.h>
#    include <UnicodeMap.h>
#    include <GfxState.h>
#    include <SplashOutputDev.h>
#    include <Stream.h>
#    include <FontInfo.h>
#    include <PDFDocEncoding.h>
#    include <OptionalContent.h>
#    include <ViewerPreferences.h>
#endif

#include "poppler.h"
#include "poppler-private.h"
#include "poppler-enums.h"
#include "poppler-input-stream.h"
#include "poppler-cached-file-loader.h"

#ifdef G_OS_WIN32
#    include <stringapiset.h>
#endif

/**
 * SECTION:poppler-document
 * @short_description: Information about a document
 * @title: PopplerDocument
 *
 * The #PopplerDocument is an object used to refer to a main document.
 */

enum
{
    PROP_0,
    PROP_TITLE,
    PROP_FORMAT,
    PROP_FORMAT_MAJOR,
    PROP_FORMAT_MINOR,
    PROP_SUBTYPE,
    PROP_SUBTYPE_STRING,
    PROP_SUBTYPE_PART,
    PROP_SUBTYPE_CONF,
    PROP_AUTHOR,
    PROP_SUBJECT,
    PROP_KEYWORDS,
    PROP_CREATOR,
    PROP_PRODUCER,
    PROP_CREATION_DATE,
    PROP_MOD_DATE,
    PROP_LINEARIZED,
    PROP_PAGE_LAYOUT,
    PROP_PAGE_MODE,
    PROP_VIEWER_PREFERENCES,
    PROP_PERMISSIONS,
    PROP_METADATA,
    PROP_PRINT_SCALING,
    PROP_PRINT_DUPLEX,
    PROP_PRINT_N_COPIES,
    PROP_CREATION_DATETIME,
    PROP_MOD_DATETIME
};

static void poppler_document_layers_free(PopplerDocument *document);

typedef struct _PopplerDocumentClass PopplerDocumentClass;
struct _PopplerDocumentClass
{
    GObjectClass parent_class;
};

G_DEFINE_TYPE(PopplerDocument, poppler_document, G_TYPE_OBJECT)

static PopplerDocument *_poppler_document_new_from_pdfdoc(std::unique_ptr<GlobalParamsIniter> &&initer, PDFDoc *newDoc, GError **error)
{
    PopplerDocument *document;

    if (!newDoc->isOk()) {
        int fopen_errno;
        switch (newDoc->getErrorCode()) {
        case errOpenFile:
            // If there was an error opening the file, count it as a G_FILE_ERROR
            // and set the GError parameters accordingly. (this assumes that the
            // only way to get an errOpenFile error is if newDoc was created using
            // a filename and thus fopen was called, which right now is true.
            fopen_errno = newDoc->getFopenErrno();
            g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(fopen_errno), "%s", g_strerror(fopen_errno));
            break;
        case errBadCatalog:
            g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_BAD_CATALOG, "Failed to read the document catalog");
            break;
        case errDamaged:
            g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_DAMAGED, "PDF document is damaged");
            break;
        case errEncrypted:
            g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_ENCRYPTED, "Document is encrypted");
            break;
        default:
            g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_INVALID, "Failed to load document");
        }

        delete newDoc;
        return nullptr;
    }

    document = (PopplerDocument *)g_object_new(POPPLER_TYPE_DOCUMENT, nullptr);
    document->initer = std::move(initer);
    document->doc = newDoc;

    document->output_dev = new CairoOutputDev();
    document->output_dev->startDoc(document->doc);

    return document;
}

static std::optional<GooString> poppler_password_to_latin1(const gchar *password)
{
    gchar *password_latin;

    if (!password) {
        return {};
    }

    password_latin = g_convert(password, -1, "ISO-8859-1", "UTF-8", nullptr, nullptr, nullptr);
    std::optional<GooString> password_g = GooString(password_latin);
    g_free(password_latin);

    return password_g;
}

/**
 * poppler_document_new_from_file:
 * @uri: uri of the file to load
 * @password: (allow-none): password to unlock the file with, or %NULL
 * @error: (allow-none): Return location for an error, or %NULL
 *
 * Creates a new #PopplerDocument.  If %NULL is returned, then @error will be
 * set. Possible errors include those in the #POPPLER_ERROR and #G_FILE_ERROR
 * domains.
 *
 * Return value: A newly created #PopplerDocument, or %NULL
 **/
PopplerDocument *poppler_document_new_from_file(const char *uri, const char *password, GError **error)
{
    PDFDoc *newDoc;
    char *filename;

    auto initer = std::make_unique<GlobalParamsIniter>(_poppler_error_cb);

    filename = g_filename_from_uri(uri, nullptr, error);
    if (!filename) {
        return nullptr;
    }

    const std::optional<GooString> password_g = poppler_password_to_latin1(password);

#ifdef G_OS_WIN32
    wchar_t *filenameW;
    int length;

    length = MultiByteToWideChar(CP_UTF8, 0, filename, -1, nullptr, 0);

    filenameW = new WCHAR[length];
    if (!filenameW)
        return nullptr;

    length = MultiByteToWideChar(CP_UTF8, 0, filename, -1, filenameW, length);

    newDoc = new PDFDoc(filenameW, length, password_g, password_g);
    if (!newDoc->isOk() && newDoc->getErrorCode() == errEncrypted && password) {
        /* Try again with original password (which comes from GTK in UTF8) Issue #824 */
        delete newDoc;
        newDoc = new PDFDoc(filenameW, length, GooString(password), GooString(password));
    }
    delete[] filenameW;
#else
    newDoc = new PDFDoc(std::make_unique<GooString>(filename), password_g, password_g);
    if (!newDoc->isOk() && newDoc->getErrorCode() == errEncrypted && password) {
        /* Try again with original password (which comes from GTK in UTF8) Issue #824 */
        delete newDoc;
        newDoc = new PDFDoc(std::make_unique<GooString>(filename), GooString(password), GooString(password));
    }
#endif
    g_free(filename);

    return _poppler_document_new_from_pdfdoc(std::move(initer), newDoc, error);
}

/**
 * poppler_document_new_from_data:
 * @data: (array length=length) (element-type guint8): the pdf data
 * @length: the length of #data
 * @password: (nullable): password to unlock the file with, or %NULL
 * @error: (nullable): Return location for an error, or %NULL
 *
 * Creates a new #PopplerDocument.  If %NULL is returned, then @error will be
 * set. Possible errors include those in the #POPPLER_ERROR and #G_FILE_ERROR
 * domains.
 *
 * Note that @data is not copied nor is a new reference to it created.
 * It must remain valid and cannot be destroyed as long as the returned
 * document exists.
 *
 * Return value: A newly created #PopplerDocument, or %NULL
 *
 * Deprecated: 0.82: This requires directly managing @length and @data.
 * Use poppler_document_new_from_bytes() instead.
 **/
PopplerDocument *poppler_document_new_from_data(char *data, int length, const char *password, GError **error)
{
    PDFDoc *newDoc;
    MemStream *str;

    auto initer = std::make_unique<GlobalParamsIniter>(_poppler_error_cb);

    // create stream
    str = new MemStream(data, 0, length, Object(objNull));

    const std::optional<GooString> password_g = poppler_password_to_latin1(password);
    newDoc = new PDFDoc(str, password_g, password_g);
    if (!newDoc->isOk() && newDoc->getErrorCode() == errEncrypted && password) {
        /* Try again with original password (which comes from GTK in UTF8) Issue #824 */
        str = dynamic_cast<MemStream *>(str->copy());
        delete newDoc;
        newDoc = new PDFDoc(str, GooString(password), GooString(password));
    }

    return _poppler_document_new_from_pdfdoc(std::move(initer), newDoc, error);
}

class BytesStream : public MemStream
{
    std::unique_ptr<GBytes, decltype(&g_bytes_unref)> m_bytes;

public:
    BytesStream(GBytes *bytes, Object &&dictA) : MemStream(static_cast<const char *>(g_bytes_get_data(bytes, nullptr)), 0, g_bytes_get_size(bytes), std::move(dictA)), m_bytes { g_bytes_ref(bytes), &g_bytes_unref } { }
    ~BytesStream() override;
};

BytesStream::~BytesStream() = default;

class OwningFileStream final : public FileStream
{
public:
    OwningFileStream(std::unique_ptr<GooFile> fileA, Object &&dictA) : FileStream(fileA.get(), 0, false, fileA->size(), std::move(dictA)), file(std::move(fileA)) { }

    ~OwningFileStream() override;

private:
    std::unique_ptr<GooFile> file;
};

OwningFileStream::~OwningFileStream() = default;

/**
 * poppler_document_new_from_bytes:
 * @bytes: a #GBytes
 * @password: (allow-none): password to unlock the file with, or %NULL
 * @error: (allow-none): Return location for an error, or %NULL
 *
 * Creates a new #PopplerDocument from @bytes. The returned document
 * will hold a reference to @bytes.
 *
 * On error,  %NULL is returned, with @error set. Possible errors include
 * those in the #POPPLER_ERROR and #G_FILE_ERROR domains.
 *
 * Return value: (transfer full): a newly created #PopplerDocument, or %NULL
 *
 * Since: 0.82
 **/
PopplerDocument *poppler_document_new_from_bytes(GBytes *bytes, const char *password, GError **error)
{
    PDFDoc *newDoc;
    BytesStream *str;

    g_return_val_if_fail(bytes != nullptr, nullptr);
    g_return_val_if_fail(error == nullptr || *error == nullptr, nullptr);

    auto initer = std::make_unique<GlobalParamsIniter>(_poppler_error_cb);

    // create stream
    str = new BytesStream(bytes, Object(objNull));

    const std::optional<GooString> password_g = poppler_password_to_latin1(password);
    newDoc = new PDFDoc(str, password_g, password_g);
    if (!newDoc->isOk() && newDoc->getErrorCode() == errEncrypted && password) {
        /* Try again with original password (which comes from GTK in UTF8) Issue #824 */
        str = dynamic_cast<BytesStream *>(str->copy());
        delete newDoc;
        newDoc = new PDFDoc(str, GooString(password), GooString(password));
    }

    return _poppler_document_new_from_pdfdoc(std::move(initer), newDoc, error);
}

static inline gboolean stream_is_memory_buffer_or_local_file(GInputStream *stream)
{
    return G_IS_MEMORY_INPUT_STREAM(stream) || (G_IS_FILE_INPUT_STREAM(stream) && strcmp(g_type_name_from_instance((GTypeInstance *)stream), "GLocalFileInputStream") == 0);
}

/**
 * poppler_document_new_from_stream:
 * @stream: a #GInputStream to read from
 * @length: the stream length, or -1 if not known
 * @password: (allow-none): password to unlock the file with, or %NULL
 * @cancellable: (allow-none): a #GCancellable, or %NULL
 * @error: (allow-none): Return location for an error, or %NULL
 *
 * Creates a new #PopplerDocument reading the PDF contents from @stream.
 * Note that the given #GInputStream must be seekable or %G_IO_ERROR_NOT_SUPPORTED
 * will be returned.
 * Possible errors include those in the #POPPLER_ERROR, #G_FILE_ERROR
 * and #G_IO_ERROR domains.
 *
 * Returns: (transfer full): a new #PopplerDocument, or %NULL
 *
 * Since: 0.22
 */
PopplerDocument *poppler_document_new_from_stream(GInputStream *stream, goffset length, const char *password, GCancellable *cancellable, GError **error)
{
    PDFDoc *newDoc;
    BaseStream *str;

    g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL);
    g_return_val_if_fail(length == (goffset)-1 || length > 0, NULL);

    auto initer = std::make_unique<GlobalParamsIniter>(_poppler_error_cb);

    if (!G_IS_SEEKABLE(stream) || !g_seekable_can_seek(G_SEEKABLE(stream))) {
        g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Stream is not seekable");
        return nullptr;
    }

    if (stream_is_memory_buffer_or_local_file(stream)) {
        if (length == (goffset)-1) {
            if (!g_seekable_seek(G_SEEKABLE(stream), 0, G_SEEK_END, cancellable, error)) {
                g_prefix_error(error, "Unable to determine length of stream: ");
                return nullptr;
            }
            length = g_seekable_tell(G_SEEKABLE(stream));
        }
        str = new PopplerInputStream(stream, cancellable, 0, false, length, Object(objNull));
    } else {
        CachedFile *cachedFile = new CachedFile(new PopplerCachedFileLoader(stream, cancellable, length));
        str = new CachedFileStream(cachedFile, 0, false, cachedFile->getLength(), Object(objNull));
    }

    const std::optional<GooString> password_g = poppler_password_to_latin1(password);
    newDoc = new PDFDoc(str, password_g, password_g);
    if (!newDoc->isOk() && newDoc->getErrorCode() == errEncrypted && password) {
        /* Try again with original password (which comes from GTK in UTF8) Issue #824 */
        str = str->copy();
        delete newDoc;
        newDoc = new PDFDoc(str, GooString(password), GooString(password));
    }

    return _poppler_document_new_from_pdfdoc(std::move(initer), newDoc, error);
}

/**
 * poppler_document_new_from_gfile:
 * @file: a #GFile to load
 * @password: (allow-none): password to unlock the file with, or %NULL
 * @cancellable: (allow-none): a #GCancellable, or %NULL
 * @error: (allow-none): Return location for an error, or %NULL
 *
 * Creates a new #PopplerDocument reading the PDF contents from @file.
 * Possible errors include those in the #POPPLER_ERROR and #G_FILE_ERROR
 * domains.
 *
 * Returns: (transfer full): a new #PopplerDocument, or %NULL
 *
 * Since: 0.22
 */
PopplerDocument *poppler_document_new_from_gfile(GFile *file, const char *password, GCancellable *cancellable, GError **error)
{
    PopplerDocument *document;
    GFileInputStream *stream;

    g_return_val_if_fail(G_IS_FILE(file), NULL);

    if (g_file_is_native(file)) {
        gchar *uri;

        uri = g_file_get_uri(file);
        document = poppler_document_new_from_file(uri, password, error);
        g_free(uri);

        return document;
    }

    stream = g_file_read(file, cancellable, error);
    if (!stream) {
        return nullptr;
    }

    document = poppler_document_new_from_stream(G_INPUT_STREAM(stream), -1, password, cancellable, error);
    g_object_unref(stream);

    return document;
}

#ifndef G_OS_WIN32

/**
 * poppler_document_new_from_fd:
 * @fd: a valid file descriptor
 * @password: (allow-none): password to unlock the file with, or %NULL
 * @error: (allow-none): Return location for an error, or %NULL
 *
 * Creates a new #PopplerDocument reading the PDF contents from the file
 * descriptor @fd. @fd must refer to a regular file, or STDIN, and be open
 * for reading.
 * Possible errors include those in the #POPPLER_ERROR and #G_FILE_ERROR
 * domains.
 * Note that this function takes ownership of @fd; you must not operate on it
 * again, nor close it.
 *
 * Returns: (transfer full): a new #PopplerDocument, or %NULL
 *
 * Since: 21.12.0
 */
PopplerDocument *poppler_document_new_from_fd(int fd, const char *password, GError **error)
{
    struct stat statbuf;
    int flags;
    BaseStream *stream;
    PDFDoc *newDoc;

    g_return_val_if_fail(fd != -1, nullptr);

    auto initer = std::make_unique<GlobalParamsIniter>(_poppler_error_cb);

    if (fstat(fd, &statbuf) == -1 || (flags = fcntl(fd, F_GETFL, &flags)) == -1) {
        int errsv = errno;
        g_set_error_literal(error, G_FILE_ERROR, g_file_error_from_errno(errsv), g_strerror(errsv));
        close(fd);
        return nullptr;
    }

    switch (flags & O_ACCMODE) {
    case O_RDONLY:
    case O_RDWR:
        break;
    case O_WRONLY:
    default:
        g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_BADF, "File descriptor %d is not readable", fd);
        close(fd);
        return nullptr;
    }

    if (fd == fileno(stdin) || !S_ISREG(statbuf.st_mode)) {
        FILE *file;
        if (fd == fileno(stdin)) {
            file = stdin;
        } else {
            file = fdopen(fd, "rb");
            if (!file) {
                int errsv = errno;
                g_set_error_literal(error, G_FILE_ERROR, g_file_error_from_errno(errsv), g_strerror(errsv));
                close(fd);
                return nullptr;
            }
        }

        CachedFile *cachedFile = new CachedFile(new FILECacheLoader(file));
        stream = new CachedFileStream(cachedFile, 0, false, cachedFile->getLength(), Object(objNull));
    } else {
        stream = new OwningFileStream(GooFile::open(fd), Object(objNull));
    }

    const std::optional<GooString> password_g = poppler_password_to_latin1(password);
    newDoc = new PDFDoc(stream, password_g, password_g);
    if (!newDoc->isOk() && newDoc->getErrorCode() == errEncrypted && password) {
        /* Try again with original password (which comes from GTK in UTF8) Issue #824 */
        stream = stream->copy();
        delete newDoc;
        newDoc = new PDFDoc(stream, GooString(password), GooString(password));
    }

    return _poppler_document_new_from_pdfdoc(std::move(initer), newDoc, error);
}

#endif /* !G_OS_WIN32 */

static gboolean handle_save_error(int err_code, GError **error)
{
    switch (err_code) {
    case errNone:
        break;
    case errOpenFile:
        g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_OPEN_FILE, "Failed to open file for writing");
        break;
    case errEncrypted:
        g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_ENCRYPTED, "Document is encrypted");
        break;
    default:
        g_set_error(error, POPPLER_ERROR, POPPLER_ERROR_INVALID, "Failed to save document");
    }

    return err_code == errNone;
}

/**
 * poppler_document_save:
 * @document: a #PopplerDocument
 * @uri: uri of file to save
 * @error: (allow-none): return location for an error, or %NULL
 *
 * Saves @document. Any change made in the document such as
 * form fields filled, annotations added or modified
 * will be saved.
 * If @error is set, %FALSE will be returned. Possible errors
 * include those in the #G_FILE_ERROR domain.
 *
 * Return value: %TRUE, if the document was successfully saved
 **/
gboolean poppler_document_save(PopplerDocument *document, const char *uri, GError **error)
{
    char *filename;
    gboolean retval = FALSE;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);

    filename = g_filename_from_uri(uri, nullptr, error);
    if (filename != nullptr) {
        GooString fname(filename);
        int err_code;
        g_free(filename);

        err_code = document->doc->saveAs(fname);
        retval = handle_save_error(err_code, error);
    }

    return retval;
}

/**
 * poppler_document_save_a_copy:
 * @document: a #PopplerDocument
 * @uri: uri of file to save
 * @error: (allow-none): return location for an error, or %NULL
 *
 * Saves a copy of the original @document.
 * Any change made in the document such as
 * form fields filled by the user will not be saved.
 * If @error is set, %FALSE will be returned. Possible errors
 * include those in the #G_FILE_ERROR domain.
 *
 * Return value: %TRUE, if the document was successfully saved
 **/
gboolean poppler_document_save_a_copy(PopplerDocument *document, const char *uri, GError **error)
{
    char *filename;
    gboolean retval = FALSE;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);

    filename = g_filename_from_uri(uri, nullptr, error);
    if (filename != nullptr) {
        GooString fname(filename);
        int err_code;
        g_free(filename);

        err_code = document->doc->saveWithoutChangesAs(fname);
        retval = handle_save_error(err_code, error);
    }

    return retval;
}

#ifndef G_OS_WIN32

/**
 * poppler_document_save_to_fd:
 * @document: a #PopplerDocument
 * @fd: a valid file descriptor open for writing
 * @include_changes: whether to include user changes (e.g. form fills)
 * @error: (allow-none): return location for an error, or %NULL
 *
 * Saves @document. Any change made in the document such as
 * form fields filled, annotations added or modified
 * will be saved if @include_changes is %TRUE, or discarded i
 * @include_changes is %FALSE.
 *
 * Note that this function takes ownership of @fd; you must not operate on it
 * again, nor close it.
 *
 * If @error is set, %FALSE will be returned. Possible errors
 * include those in the #G_FILE_ERROR domain.
 *
 * Return value: %TRUE, if the document was successfully saved
 *
 * Since: 21.12.0
 **/
gboolean poppler_document_save_to_fd(PopplerDocument *document, int fd, gboolean include_changes, GError **error)
{
    FILE *file;
    OutStream *stream;
    int rv;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);
    g_return_val_if_fail(fd != -1, FALSE);

    file = fdopen(fd, "wb");
    if (file == nullptr) {
        int errsv = errno;
        g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errsv), "Failed to open FD %d for writing: %s", fd, g_strerror(errsv));
        return FALSE;
    }

    stream = new FileOutStream(file, 0);
    if (include_changes) {
        rv = document->doc->saveAs(stream);
    } else {
        rv = document->doc->saveWithoutChangesAs(stream);
    }
    delete stream;

    return handle_save_error(rv, error);
}

#endif /* !G_OS_WIN32 */

static void poppler_document_finalize(GObject *object)
{
    PopplerDocument *document = POPPLER_DOCUMENT(object);

    poppler_document_layers_free(document);
    delete document->output_dev;
    delete document->doc;
    delete document->initer.release();

    G_OBJECT_CLASS(poppler_document_parent_class)->finalize(object);
}

/**
 * poppler_document_get_id:
 * @document: A #PopplerDocument
 * @permanent_id: (out) (allow-none): location to store an allocated string, use g_free() to free the returned string
 * @update_id: (out) (allow-none): location to store an allocated string, use g_free() to free the returned string
 *
 * Returns the PDF file identifier represented as two byte string arrays of size 32.
 * @permanent_id is the permanent identifier that is built based on the file
 * contents at the time it was originally created, so that this identifer
 * never changes. @update_id is the update identifier that is built based on
 * the file contents at the time it was last updated.
 *
 * Note that returned strings are not null-terminated, they have a fixed
 * size of 32 bytes.
 *
 * Returns: %TRUE if the @document contains an id, %FALSE otherwise
 *
 * Since: 0.16
 */
gboolean poppler_document_get_id(PopplerDocument *document, gchar **permanent_id, gchar **update_id)
{
    GooString permanent;
    GooString update;
    gboolean retval = FALSE;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);

    if (permanent_id) {
        *permanent_id = nullptr;
    }
    if (update_id) {
        *update_id = nullptr;
    }

    if (document->doc->getID(permanent_id ? &permanent : nullptr, update_id ? &update : nullptr)) {
        if (permanent_id) {
            *permanent_id = g_new(char, 32);
            memcpy(*permanent_id, permanent.c_str(), 32);
        }
        if (update_id) {
            *update_id = g_new(char, 32);
            memcpy(*update_id, update.c_str(), 32);
        }

        retval = TRUE;
    }

    return retval;
}

/**
 * poppler_document_get_n_pages:
 * @document: A #PopplerDocument
 *
 * Returns the number of pages in a loaded document.
 *
 * Return value: Number of pages
 **/
int poppler_document_get_n_pages(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), 0);

    return document->doc->getNumPages();
}

/**
 * poppler_document_get_page:
 * @document: A #PopplerDocument
 * @index: a page index
 *
 * Returns the #PopplerPage indexed at @index.  This object is owned by the
 * caller.
 *
 * Return value: (transfer full) : The #PopplerPage at @index
 **/
PopplerPage *poppler_document_get_page(PopplerDocument *document, int index)
{
    Page *page;

    g_return_val_if_fail(0 <= index && index < poppler_document_get_n_pages(document), NULL);

    page = document->doc->getPage(index + 1);
    if (!page) {
        return nullptr;
    }

    return _poppler_page_new(document, page, index);
}

/**
 * poppler_document_get_page_by_label:
 * @document: A #PopplerDocument
 * @label: a page label
 *
 * Returns the #PopplerPage reference by @label.  This object is owned by the
 * caller.  @label is a human-readable string representation of the page number,
 * and can be document specific.  Typically, it is a value such as "iii" or "3".
 *
 * By default, "1" refers to the first page.
 *
 * Return value: (transfer full) :The #PopplerPage referenced by @label
 **/
PopplerPage *poppler_document_get_page_by_label(PopplerDocument *document, const char *label)
{
    Catalog *catalog;
    GooString label_g(label);
    int index;

    catalog = document->doc->getCatalog();
    if (!catalog->labelToIndex(&label_g, &index)) {
        return nullptr;
    }

    return poppler_document_get_page(document, index);
}

/**
 * poppler_document_get_n_attachments:
 * @document: A #PopplerDocument
 *
 * Returns the number of attachments in a loaded document.
 * See also poppler_document_get_attachments()
 *
 * Return value: Number of attachments
 *
 * Since: 0.18
 */
guint poppler_document_get_n_attachments(PopplerDocument *document)
{
    Catalog *catalog;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), 0);

    catalog = document->doc->getCatalog();

    return catalog && catalog->isOk() ? catalog->numEmbeddedFiles() : 0;
}

/**
 * poppler_document_has_attachments:
 * @document: A #PopplerDocument
 *
 * Returns %TRUE of @document has any attachments.
 *
 * Return value: %TRUE, if @document has attachments.
 **/
gboolean poppler_document_has_attachments(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);

    return (poppler_document_get_n_attachments(document) != 0);
}

/**
 * poppler_document_get_attachments:
 * @document: A #PopplerDocument
 *
 * Returns a #GList containing #PopplerAttachment<!-- -->s.  These attachments
 * are unowned, and must be unreffed, and the list must be freed with
 * g_list_free().
 *
 * Return value: (element-type PopplerAttachment) (transfer full): a list of available attachments.
 **/
GList *poppler_document_get_attachments(PopplerDocument *document)
{
    Catalog *catalog;
    int n_files, i;
    GList *retval = nullptr;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    catalog = document->doc->getCatalog();
    if (catalog == nullptr || !catalog->isOk()) {
        return nullptr;
    }

    n_files = catalog->numEmbeddedFiles();
    for (i = 0; i < n_files; i++) {
        PopplerAttachment *attachment;

        const std::unique_ptr<FileSpec> emb_file = catalog->embeddedFile(i);
        if (!emb_file->isOk() || !emb_file->getEmbeddedFile()->isOk()) {
            continue;
        }

        attachment = _poppler_attachment_new(emb_file.get());

        if (attachment != nullptr) {
            retval = g_list_prepend(retval, attachment);
        }
    }
    return g_list_reverse(retval);
}

/**
 * poppler_named_dest_from_bytestring:
 * @data: (array length=length): the bytestring data
 * @length: the bytestring length
 *
 * Converts a bytestring into a zero-terminated string suitable to
 * pass to poppler_document_find_dest().
 *
 * Note that the returned string has no defined encoding and is not
 * suitable for display to the user.
 *
 * The returned data must be freed using g_free().
 *
 * Returns: (transfer full): the named dest
 *
 * Since: 0.73
 */
char *poppler_named_dest_from_bytestring(const guint8 *data, gsize length)
{
    const guint8 *p, *pend;
    char *dest, *q;

    g_return_val_if_fail(length != 0 || data != nullptr, nullptr);
    /* Each source byte needs maximally 2 destination chars (\\ or \0) */
    q = dest = (gchar *)g_malloc(length * 2 + 1);

    pend = data + length;
    for (p = data; p < pend; ++p) {
        switch (*p) {
        case '\0':
            *q++ = '\\';
            *q++ = '0';
            break;
        case '\\':
            *q++ = '\\';
            *q++ = '\\';
            break;
        default:
            *q++ = *p;
            break;
        }
    }

    *q = 0; /* zero terminate */
    return dest;
}

/**
 * poppler_named_dest_to_bytestring:
 * @name: the named dest string
 * @length: (out): a location to store the length of the returned bytestring
 *
 * Converts a named dest string (e.g. from #PopplerDest.named_dest) into a
 * bytestring, inverting the transformation of
 * poppler_named_dest_from_bytestring().
 *
 * Note that the returned data is not zero terminated and may also
 * contains embedded NUL bytes.
 *
 * If @name is not a valid named dest string, returns %NULL.
 *
 * The returned data must be freed using g_free().
 *
 * Returns: (array length=length) (transfer full) (nullable): a new bytestring,
 *   or %NULL
 *
 * Since: 0.73
 */
guint8 *poppler_named_dest_to_bytestring(const char *name, gsize *length)
{
    const char *p;
    guint8 *data, *q;
    gsize len;

    g_return_val_if_fail(name != nullptr, nullptr);
    g_return_val_if_fail(length != nullptr, nullptr);

    len = strlen(name);
    q = data = (guint8 *)g_malloc(len);
    for (p = name; *p; ++p) {
        if (*p == '\\') {
            p++;
            len--;
            if (*p == '0') {
                *q++ = '\0';
            } else if (*p == '\\') {
                *q++ = '\\';
            } else {
                goto invalid;
            }
        } else {
            *q++ = *p;
        }
    }

    *length = len;
    return data;

invalid:
    g_free(data);
    *length = 0;
    return nullptr;
}

/**
 * poppler_document_find_dest:
 * @document: A #PopplerDocument
 * @link_name: a named destination
 *
 * Creates a #PopplerDest for the named destination @link_name in @document.
 *
 * Note that named destinations are bytestrings, not string. That means that
 * unless @link_name was returned by a poppler function (e.g. is
 * #PopplerDest.named_dest), it needs to be converted to string
 * using poppler_named_dest_from_bytestring() before being passed to this
 * function.
 *
 * The returned value must be freed with poppler_dest_free().
 *
 * Return value: (transfer full): a new #PopplerDest destination, or %NULL if
 *   @link_name is not a destination.
 **/
PopplerDest *poppler_document_find_dest(PopplerDocument *document, const gchar *link_name)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), nullptr);
    g_return_val_if_fail(link_name != nullptr, nullptr);

    gsize len;
    guint8 *data = poppler_named_dest_to_bytestring(link_name, &len);
    if (data == nullptr) {
        return nullptr;
    }

    GooString g_link_name((const char *)data, (int)len);
    g_free(data);

    std::unique_ptr<LinkDest> link_dest = document->doc->findDest(&g_link_name);
    if (link_dest == nullptr) {
        return nullptr;
    }

    PopplerDest *dest = _poppler_dest_new_goto(document, link_dest.get());

    return dest;
}

static gint _poppler_dest_compare_keys(gconstpointer a, gconstpointer b, gpointer user_data)
{
    return g_strcmp0(static_cast<const gchar *>(a), static_cast<const gchar *>(b));
}

static void _poppler_dest_destroy_value(gpointer value)
{
    poppler_dest_free(static_cast<PopplerDest *>(value));
}

/**
 * poppler_document_create_dests_tree:
 * @document: A #PopplerDocument
 *
 * Creates a balanced binary tree of all named destinations in @document
 *
 * The tree key is strings in the form returned by
 * poppler_named_dest_to_bytestring() which constains a destination name.
 * The tree value is the #PopplerDest which contains a named destination.
 * The return value must be freed with g_tree_destroy().
 *
 * Returns: (transfer full) (nullable): the #GTree, or %NULL
 * Since: 0.78
 **/
GTree *poppler_document_create_dests_tree(PopplerDocument *document)
{
    GTree *tree;
    Catalog *catalog;
    PopplerDest *dest;
    int i;
    gchar *key;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), nullptr);

    catalog = document->doc->getCatalog();
    if (catalog == nullptr) {
        return nullptr;
    }

    tree = g_tree_new_full(_poppler_dest_compare_keys, nullptr, g_free, _poppler_dest_destroy_value);

    // Iterate from name-dict
    const int nDests = catalog->numDests();
    for (i = 0; i < nDests; ++i) {
        // The names of name-dict cannot contain \0,
        // so we can use strlen().
        auto name = catalog->getDestsName(i);
        key = poppler_named_dest_from_bytestring(reinterpret_cast<const guint8 *>(name), strlen(name));
        std::unique_ptr<LinkDest> link_dest = catalog->getDestsDest(i);
        if (link_dest) {
            dest = _poppler_dest_new_goto(document, link_dest.get());
            g_tree_insert(tree, key, dest);
        }
    }

    // Iterate form name-tree
    const int nDestsNameTree = catalog->numDestNameTree();
    for (i = 0; i < nDestsNameTree; ++i) {
        auto name = catalog->getDestNameTreeName(i);
        key = poppler_named_dest_from_bytestring(reinterpret_cast<const guint8 *>(name->c_str()), name->getLength());
        std::unique_ptr<LinkDest> link_dest = catalog->getDestNameTreeDest(i);
        if (link_dest) {
            dest = _poppler_dest_new_goto(document, link_dest.get());
            g_tree_insert(tree, key, dest);
        }
    }

    return tree;
}

char *_poppler_goo_string_to_utf8(const GooString *s)
{
    if (s == nullptr) {
        return nullptr;
    }

    char *result;

    if (s->hasUnicodeMarker()) {
        result = g_convert(s->c_str() + 2, s->getLength() - 2, "UTF-8", "UTF-16BE", nullptr, nullptr, nullptr);
    } else if (s->hasUnicodeMarkerLE()) {
        result = g_convert(s->c_str() + 2, s->getLength() - 2, "UTF-8", "UTF-16LE", nullptr, nullptr, nullptr);
    } else {
        int len;
        gunichar *ucs4_temp;
        int i;

        len = s->getLength();
        ucs4_temp = g_new(gunichar, len + 1);
        for (i = 0; i < len; ++i) {
            ucs4_temp[i] = pdfDocEncoding[(unsigned char)s->getChar(i)];
        }
        ucs4_temp[i] = 0;

        result = g_ucs4_to_utf8(ucs4_temp, -1, nullptr, nullptr, nullptr);

        g_free(ucs4_temp);
    }

    return result;
}

static GooString *_poppler_goo_string_from_utf8(const gchar *src)
{
    if (src == nullptr) {
        return nullptr;
    }

    gsize outlen;

    gchar *utf16 = g_convert(src, -1, "UTF-16BE", "UTF-8", nullptr, &outlen, nullptr);
    if (utf16 == nullptr) {
        return nullptr;
    }

    GooString *result = new GooString(utf16, outlen);
    g_free(utf16);

    if (!result->hasUnicodeMarker()) {
        result->prependUnicodeMarker();
    }

    return result;
}

static PopplerPageLayout convert_page_layout(Catalog::PageLayout pageLayout)
{
    switch (pageLayout) {
    case Catalog::pageLayoutSinglePage:
        return POPPLER_PAGE_LAYOUT_SINGLE_PAGE;
    case Catalog::pageLayoutOneColumn:
        return POPPLER_PAGE_LAYOUT_ONE_COLUMN;
    case Catalog::pageLayoutTwoColumnLeft:
        return POPPLER_PAGE_LAYOUT_TWO_COLUMN_LEFT;
    case Catalog::pageLayoutTwoColumnRight:
        return POPPLER_PAGE_LAYOUT_TWO_COLUMN_RIGHT;
    case Catalog::pageLayoutTwoPageLeft:
        return POPPLER_PAGE_LAYOUT_TWO_PAGE_LEFT;
    case Catalog::pageLayoutTwoPageRight:
        return POPPLER_PAGE_LAYOUT_TWO_PAGE_RIGHT;
    case Catalog::pageLayoutNone:
    default:
        return POPPLER_PAGE_LAYOUT_UNSET;
    }
}

static PopplerPageMode convert_page_mode(Catalog::PageMode pageMode)
{
    switch (pageMode) {
    case Catalog::pageModeOutlines:
        return POPPLER_PAGE_MODE_USE_OUTLINES;
    case Catalog::pageModeThumbs:
        return POPPLER_PAGE_MODE_USE_THUMBS;
    case Catalog::pageModeFullScreen:
        return POPPLER_PAGE_MODE_FULL_SCREEN;
    case Catalog::pageModeOC:
        return POPPLER_PAGE_MODE_USE_OC;
    case Catalog::pageModeAttach:
        return POPPLER_PAGE_MODE_USE_ATTACHMENTS;
    case Catalog::pageModeNone:
    default:
        return POPPLER_PAGE_MODE_UNSET;
    }
}

static PopplerPDFSubtype convert_pdf_subtype(PDFSubtype pdfSubtype)
{
    switch (pdfSubtype) {
    case subtypePDFA:
        return POPPLER_PDF_SUBTYPE_PDF_A;
    case subtypePDFE:
        return POPPLER_PDF_SUBTYPE_PDF_E;
    case subtypePDFUA:
        return POPPLER_PDF_SUBTYPE_PDF_UA;
    case subtypePDFVT:
        return POPPLER_PDF_SUBTYPE_PDF_VT;
    case subtypePDFX:
        return POPPLER_PDF_SUBTYPE_PDF_X;
    case subtypeNone:
        return POPPLER_PDF_SUBTYPE_NONE;
    case subtypeNull:
    default:
        return POPPLER_PDF_SUBTYPE_UNSET;
    }
}

static PopplerPDFPart convert_pdf_subtype_part(PDFSubtypePart pdfSubtypePart)
{
    switch (pdfSubtypePart) {
    case subtypePart1:
        return POPPLER_PDF_SUBTYPE_PART_1;
    case subtypePart2:
        return POPPLER_PDF_SUBTYPE_PART_2;
    case subtypePart3:
        return POPPLER_PDF_SUBTYPE_PART_3;
    case subtypePart4:
        return POPPLER_PDF_SUBTYPE_PART_4;
    case subtypePart5:
        return POPPLER_PDF_SUBTYPE_PART_5;
    case subtypePart6:
        return POPPLER_PDF_SUBTYPE_PART_6;
    case subtypePart7:
        return POPPLER_PDF_SUBTYPE_PART_7;
    case subtypePart8:
        return POPPLER_PDF_SUBTYPE_PART_8;
    case subtypePartNone:
        return POPPLER_PDF_SUBTYPE_PART_NONE;
    case subtypePartNull:
    default:
        return POPPLER_PDF_SUBTYPE_PART_UNSET;
    }
}

static PopplerPDFConformance convert_pdf_subtype_conformance(PDFSubtypeConformance pdfSubtypeConf)
{
    switch (pdfSubtypeConf) {
    case subtypeConfA:
        return POPPLER_PDF_SUBTYPE_CONF_A;
    case subtypeConfB:
        return POPPLER_PDF_SUBTYPE_CONF_B;
    case subtypeConfG:
        return POPPLER_PDF_SUBTYPE_CONF_G;
    case subtypeConfN:
        return POPPLER_PDF_SUBTYPE_CONF_N;
    case subtypeConfP:
        return POPPLER_PDF_SUBTYPE_CONF_P;
    case subtypeConfPG:
        return POPPLER_PDF_SUBTYPE_CONF_PG;
    case subtypeConfU:
        return POPPLER_PDF_SUBTYPE_CONF_U;
    case subtypeConfNone:
        return POPPLER_PDF_SUBTYPE_CONF_NONE;
    case subtypeConfNull:
    default:
        return POPPLER_PDF_SUBTYPE_CONF_UNSET;
    }
}

/**
 * poppler_document_get_pdf_version_string:
 * @document: A #PopplerDocument
 *
 * Returns the PDF version of @document as a string (e.g. PDF-1.6)
 *
 * Return value: a new allocated string containing the PDF version
 *               of @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_pdf_version_string(PopplerDocument *document)
{
    gchar *retval;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    retval = g_strndup("PDF-", 15); /* allocates 16 chars, pads with \0s */
    g_ascii_formatd(retval + 4, 15 + 1 - 4, "%.2g", document->doc->getPDFMajorVersion() + document->doc->getPDFMinorVersion() / 10.0);
    return retval;
}

/**
 * poppler_document_get_pdf_version:
 * @document: A #PopplerDocument
 * @major_version: (out) (nullable): return location for the PDF major version number
 * @minor_version: (out) (nullable): return location for the PDF minor version number
 *
 * Updates values referenced by @major_version & @minor_version with the
 * major and minor PDF versions of @document.
 *
 * Since: 0.16
 **/
void poppler_document_get_pdf_version(PopplerDocument *document, guint *major_version, guint *minor_version)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    if (major_version) {
        *major_version = document->doc->getPDFMajorVersion();
    }
    if (minor_version) {
        *minor_version = document->doc->getPDFMinorVersion();
    }
}

/**
 * poppler_document_get_title:
 * @document: A #PopplerDocument
 *
 * Returns the document's title
 *
 * Return value: a new allocated string containing the title
 *               of @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_title(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    const std::unique_ptr<GooString> goo_title = document->doc->getDocInfoTitle();
    return _poppler_goo_string_to_utf8(goo_title.get());
}

/**
 * poppler_document_set_title:
 * @document: A #PopplerDocument
 * @title: A new title
 *
 * Sets the document's title. If @title is %NULL, Title entry
 * is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_title(PopplerDocument *document, const gchar *title)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *goo_title;
    if (!title) {
        goo_title = nullptr;
    } else {
        goo_title = _poppler_goo_string_from_utf8(title);
        if (!goo_title) {
            return;
        }
    }
    document->doc->setDocInfoTitle(goo_title);
}

/**
 * poppler_document_get_author:
 * @document: A #PopplerDocument
 *
 * Returns the author of the document
 *
 * Return value: a new allocated string containing the author
 *               of @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_author(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    const std::unique_ptr<GooString> goo_author = document->doc->getDocInfoAuthor();
    return _poppler_goo_string_to_utf8(goo_author.get());
}

/**
 * poppler_document_set_author:
 * @document: A #PopplerDocument
 * @author: A new author
 *
 * Sets the document's author. If @author is %NULL, Author
 * entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_author(PopplerDocument *document, const gchar *author)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *goo_author;
    if (!author) {
        goo_author = nullptr;
    } else {
        goo_author = _poppler_goo_string_from_utf8(author);
        if (!goo_author) {
            return;
        }
    }
    document->doc->setDocInfoAuthor(goo_author);
}

/**
 * poppler_document_get_subject:
 * @document: A #PopplerDocument
 *
 * Returns the subject of the document
 *
 * Return value: a new allocated string containing the subject
 *               of @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_subject(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    const std::unique_ptr<GooString> goo_subject = document->doc->getDocInfoSubject();
    return _poppler_goo_string_to_utf8(goo_subject.get());
}

/**
 * poppler_document_set_subject:
 * @document: A #PopplerDocument
 * @subject: A new subject
 *
 * Sets the document's subject. If @subject is %NULL, Subject
 * entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_subject(PopplerDocument *document, const gchar *subject)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *goo_subject;
    if (!subject) {
        goo_subject = nullptr;
    } else {
        goo_subject = _poppler_goo_string_from_utf8(subject);
        if (!goo_subject) {
            return;
        }
    }
    document->doc->setDocInfoSubject(goo_subject);
}

/**
 * poppler_document_get_keywords:
 * @document: A #PopplerDocument
 *
 * Returns the keywords associated to the document
 *
 * Return value: a new allocated string containing keywords associated
 *               to @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_keywords(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    const std::unique_ptr<GooString> goo_keywords = document->doc->getDocInfoKeywords();
    return _poppler_goo_string_to_utf8(goo_keywords.get());
}

/**
 * poppler_document_set_keywords:
 * @document: A #PopplerDocument
 * @keywords: New keywords
 *
 * Sets the document's keywords. If @keywords is %NULL,
 * Keywords entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_keywords(PopplerDocument *document, const gchar *keywords)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *goo_keywords;
    if (!keywords) {
        goo_keywords = nullptr;
    } else {
        goo_keywords = _poppler_goo_string_from_utf8(keywords);
        if (!goo_keywords) {
            return;
        }
    }
    document->doc->setDocInfoKeywords(goo_keywords);
}

/**
 * poppler_document_get_creator:
 * @document: A #PopplerDocument
 *
 * Returns the creator of the document. If the document was converted
 * from another format, the creator is the name of the product
 * that created the original document from which it was converted.
 *
 * Return value: a new allocated string containing the creator
 *               of @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_creator(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    const std::unique_ptr<GooString> goo_creator = document->doc->getDocInfoCreator();
    return _poppler_goo_string_to_utf8(goo_creator.get());
}

/**
 * poppler_document_set_creator:
 * @document: A #PopplerDocument
 * @creator: A new creator
 *
 * Sets the document's creator. If @creator is %NULL, Creator
 * entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_creator(PopplerDocument *document, const gchar *creator)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *goo_creator;
    if (!creator) {
        goo_creator = nullptr;
    } else {
        goo_creator = _poppler_goo_string_from_utf8(creator);
        if (!goo_creator) {
            return;
        }
    }
    document->doc->setDocInfoCreator(goo_creator);
}

/**
 * poppler_document_get_producer:
 * @document: A #PopplerDocument
 *
 * Returns the producer of the document. If the document was converted
 * from another format, the producer is the name of the product
 * that converted it to PDF
 *
 * Return value: a new allocated string containing the producer
 *               of @document, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_producer(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    const std::unique_ptr<GooString> goo_producer = document->doc->getDocInfoProducer();
    return _poppler_goo_string_to_utf8(goo_producer.get());
}

/**
 * poppler_document_set_producer:
 * @document: A #PopplerDocument
 * @producer: A new producer
 *
 * Sets the document's producer. If @producer is %NULL,
 * Producer entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_producer(PopplerDocument *document, const gchar *producer)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *goo_producer;
    if (!producer) {
        goo_producer = nullptr;
    } else {
        goo_producer = _poppler_goo_string_from_utf8(producer);
        if (!goo_producer) {
            return;
        }
    }
    document->doc->setDocInfoProducer(goo_producer);
}

/**
 * poppler_document_get_creation_date:
 * @document: A #PopplerDocument
 *
 * Returns the date the document was created as seconds since the Epoch
 *
 * Return value: the date the document was created, or -1
 *
 * Since: 0.16
 **/
time_t poppler_document_get_creation_date(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), (time_t)-1);

    const std::unique_ptr<GooString> str = document->doc->getDocInfoCreatDate();
    if (!str) {
        return (time_t)-1;
    }

    time_t date;
    gboolean success = _poppler_convert_pdf_date_to_gtime(str.get(), &date);

    return (success) ? date : (time_t)-1;
}

/**
 * poppler_document_set_creation_date:
 * @document: A #PopplerDocument
 * @creation_date: A new creation date
 *
 * Sets the document's creation date. If @creation_date is -1, CreationDate
 * entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_creation_date(PopplerDocument *document, time_t creation_date)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *str = creation_date == (time_t)-1 ? nullptr : timeToDateString(&creation_date);
    document->doc->setDocInfoCreatDate(str);
}

/**
 * poppler_document_get_creation_date_time:
 * @document: A #PopplerDocument
 *
 * Returns the date the document was created as a #GDateTime
 *
 * Returns: (nullable): the date the document was created, or %NULL
 *
 * Since: 20.09.0
 **/
GDateTime *poppler_document_get_creation_date_time(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), nullptr);

    std::unique_ptr<GooString> str { document->doc->getDocInfoCreatDate() };

    if (!str) {
        return nullptr;
    }

    return _poppler_convert_pdf_date_to_date_time(str.get());
}

/**
 * poppler_document_set_creation_date_time:
 * @document: A #PopplerDocument
 * @creation_datetime: (nullable): A new creation #GDateTime
 *
 * Sets the document's creation date. If @creation_datetime is %NULL,
 * CreationDate entry is removed from the document's Info dictionary.
 *
 * Since: 20.09.0
 **/
void poppler_document_set_creation_date_time(PopplerDocument *document, GDateTime *creation_datetime)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *str = nullptr;

    if (creation_datetime) {
        str = _poppler_convert_date_time_to_pdf_date(creation_datetime);
    }

    document->doc->setDocInfoCreatDate(str);
}

/**
 * poppler_document_get_modification_date:
 * @document: A #PopplerDocument
 *
 * Returns the date the document was most recently modified as seconds since the Epoch
 *
 * Return value: the date the document was most recently modified, or -1
 *
 * Since: 0.16
 **/
time_t poppler_document_get_modification_date(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), (time_t)-1);

    const std::unique_ptr<GooString> str = document->doc->getDocInfoModDate();
    if (!str) {
        return (time_t)-1;
    }

    time_t date;
    gboolean success = _poppler_convert_pdf_date_to_gtime(str.get(), &date);

    return (success) ? date : (time_t)-1;
}

/**
 * poppler_document_set_modification_date:
 * @document: A #PopplerDocument
 * @modification_date: A new modification date
 *
 * Sets the document's modification date. If @modification_date is -1, ModDate
 * entry is removed from the document's Info dictionary.
 *
 * Since: 0.46
 **/
void poppler_document_set_modification_date(PopplerDocument *document, time_t modification_date)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *str = modification_date == (time_t)-1 ? nullptr : timeToDateString(&modification_date);
    document->doc->setDocInfoModDate(str);
}

/**
 * poppler_document_get_modification_date_time:
 * @document: A #PopplerDocument
 *
 * Returns the date the document was most recently modified as a #GDateTime
 *
 * Returns: (nullable): the date the document was modified, or %NULL
 *
 * Since: 20.09.0
 **/
GDateTime *poppler_document_get_modification_date_time(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), nullptr);

    std::unique_ptr<GooString> str { document->doc->getDocInfoModDate() };

    if (!str) {
        return nullptr;
    }

    return _poppler_convert_pdf_date_to_date_time(str.get());
}

/**
 * poppler_document_set_modification_date_time:
 * @document: A #PopplerDocument
 * @modification_datetime: (nullable): A new modification #GDateTime
 *
 * Sets the document's modification date. If @modification_datetime is %NULL,
 * ModDate entry is removed from the document's Info dictionary.
 *
 * Since: 20.09.0
 **/
void poppler_document_set_modification_date_time(PopplerDocument *document, GDateTime *modification_datetime)
{
    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    GooString *str = nullptr;

    if (modification_datetime) {
        str = _poppler_convert_date_time_to_pdf_date(modification_datetime);
    }

    document->doc->setDocInfoModDate(str);
}

/**
 * poppler_document_is_linearized:
 * @document: A #PopplerDocument
 *
 * Returns whether @document is linearized or not. Linearization of PDF
 * enables efficient incremental access of the PDF file in a network environment.
 *
 * Return value: %TRUE if @document is linearized, %FALSE otherwise
 *
 * Since: 0.16
 **/
gboolean poppler_document_is_linearized(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);

    return document->doc->isLinearized();
}

/**
 * poppler_document_get_n_signatures:
 * @document: A #PopplerDocument
 *
 * Returns how many digital signatures @document contains.
 * PDF digital signatures ensure that the content hash not been altered since last edit and
 * that it was produced by someone the user can trust
 *
 * Return value: The number of signatures found in the document
 *
 * Since: 21.12.0
 **/
gint poppler_document_get_n_signatures(const PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), 0);

    return document->doc->getSignatureFields().size();
}

/**
 * poppler_document_get_signature_fields:
 * @document: A #PopplerDocument
 *
 * Returns a #GList containing all signature #PopplerFormField<!-- -->s in the document.
 *
 * Return value: (element-type PopplerFormField) (transfer full): a list of all signature form fields.
 *
 * Since: 22.02.0
 **/
GList *poppler_document_get_signature_fields(PopplerDocument *document)
{
    std::vector<FormFieldSignature *> signature_fields;
    FormWidget *widget;
    GList *result = nullptr;
    gsize i;

    signature_fields = document->doc->getSignatureFields();

    for (i = 0; i < signature_fields.size(); i++) {
        widget = signature_fields[i]->getCreateWidget();

        if (widget != nullptr) {
            result = g_list_prepend(result, _poppler_form_field_new(document, widget));
        }
    }

    return g_list_reverse(result);
}

/**
 * poppler_document_get_page_layout:
 * @document: A #PopplerDocument
 *
 * Returns the page layout that should be used when the document is opened
 *
 * Return value: a #PopplerPageLayout that should be used when the document is opened
 *
 * Since: 0.16
 **/
PopplerPageLayout poppler_document_get_page_layout(PopplerDocument *document)
{
    Catalog *catalog;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PAGE_LAYOUT_UNSET);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        return convert_page_layout(catalog->getPageLayout());
    }

    return POPPLER_PAGE_LAYOUT_UNSET;
}

/**
 * poppler_document_get_page_mode:
 * @document: A #PopplerDocument
 *
 * Returns a #PopplerPageMode representing how the document should
 * be initially displayed when opened.
 *
 * Return value: a #PopplerPageMode that should be used when document is opened
 *
 * Since: 0.16
 **/
PopplerPageMode poppler_document_get_page_mode(PopplerDocument *document)
{
    Catalog *catalog;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PAGE_MODE_UNSET);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        return convert_page_mode(catalog->getPageMode());
    }

    return POPPLER_PAGE_MODE_UNSET;
}

/**
 * poppler_document_get_print_scaling:
 * @document: A #PopplerDocument
 *
 * Returns the print scaling value suggested by author of the document.
 *
 * Return value: a #PopplerPrintScaling that should be used when document is printed
 *
 * Since: 0.73
 **/
PopplerPrintScaling poppler_document_get_print_scaling(PopplerDocument *document)
{
    Catalog *catalog;
    ViewerPreferences *preferences;
    PopplerPrintScaling print_scaling = POPPLER_PRINT_SCALING_APP_DEFAULT;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PRINT_SCALING_APP_DEFAULT);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        preferences = catalog->getViewerPreferences();
        if (preferences) {
            switch (preferences->getPrintScaling()) {
            default:
            case ViewerPreferences::PrintScaling::printScalingAppDefault:
                print_scaling = POPPLER_PRINT_SCALING_APP_DEFAULT;
                break;
            case ViewerPreferences::PrintScaling::printScalingNone:
                print_scaling = POPPLER_PRINT_SCALING_NONE;
                break;
            }
        }
    }

    return print_scaling;
}

/**
 * poppler_document_get_print_duplex:
 * @document: A #PopplerDocument
 *
 * Returns the duplex mode value suggested for printing by author of the document.
 * Value POPPLER_PRINT_DUPLEX_NONE means that the document does not specify this
 * preference.
 *
 * Returns: a #PopplerPrintDuplex that should be used when document is printed
 *
 * Since: 0.80
 **/
PopplerPrintDuplex poppler_document_get_print_duplex(PopplerDocument *document)
{
    Catalog *catalog;
    ViewerPreferences *preferences;
    PopplerPrintDuplex duplex = POPPLER_PRINT_DUPLEX_NONE;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PRINT_DUPLEX_NONE);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        preferences = catalog->getViewerPreferences();
        if (preferences) {
            switch (preferences->getDuplex()) {
            default:
            case ViewerPreferences::Duplex::duplexNone:
                duplex = POPPLER_PRINT_DUPLEX_NONE;
                break;
            case ViewerPreferences::Duplex::duplexSimplex:
                duplex = POPPLER_PRINT_DUPLEX_SIMPLEX;
                break;
            case ViewerPreferences::Duplex::duplexDuplexFlipShortEdge:
                duplex = POPPLER_PRINT_DUPLEX_DUPLEX_FLIP_SHORT_EDGE;
                break;
            case ViewerPreferences::Duplex::duplexDuplexFlipLongEdge:
                duplex = POPPLER_PRINT_DUPLEX_DUPLEX_FLIP_LONG_EDGE;
                break;
            }
        }
    }

    return duplex;
}

/**
 * poppler_document_get_print_n_copies:
 * @document: A #PopplerDocument
 *
 * Returns the suggested number of copies to be printed.
 * This preference should be applied only if returned value
 * is greater than 1 since value 1 usually means that
 * the document does not specify it.
 *
 * Returns: Number of copies
 *
 * Since: 0.80
 **/
gint poppler_document_get_print_n_copies(PopplerDocument *document)
{
    Catalog *catalog;
    ViewerPreferences *preferences;
    gint retval = 1;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), 1);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        preferences = catalog->getViewerPreferences();
        if (preferences) {
            retval = preferences->getNumCopies();
        }
    }

    return retval;
}

/**
 * poppler_document_get_print_page_ranges:
 * @document: A #PopplerDocument
 * @n_ranges: (out): return location for number of ranges
 *
 * Returns the suggested page ranges to print in the form of array
 * of #PopplerPageRange<!-- -->s and number of ranges.
 * %NULL pointer means that the document does not specify page ranges
 * for printing.
 *
 * Returns: (array length=n_ranges) (transfer full): an array
 *          of #PopplerPageRange<!-- -->s or %NULL. Free the array when
 *          it is no longer needed.
 *
 * Since: 0.80
 **/
PopplerPageRange *poppler_document_get_print_page_ranges(PopplerDocument *document, int *n_ranges)
{
    Catalog *catalog;
    ViewerPreferences *preferences;
    std::vector<std::pair<int, int>> ranges;
    PopplerPageRange *result = nullptr;

    g_return_val_if_fail(n_ranges != nullptr, nullptr);
    *n_ranges = 0;
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), nullptr);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        preferences = catalog->getViewerPreferences();
        if (preferences) {
            ranges = preferences->getPrintPageRange();

            *n_ranges = ranges.size();
            result = g_new(PopplerPageRange, ranges.size());
            for (size_t i = 0; i < ranges.size(); ++i) {
                result[i].start_page = ranges[i].first;
                result[i].end_page = ranges[i].second;
            }
        }
    }

    return result;
}

/**
 * poppler_document_get_permissions:
 * @document: A #PopplerDocument
 *
 * Returns the flags specifying which operations are permitted when the document is opened.
 *
 * Return value: a set of flags from  #PopplerPermissions enumeration
 *
 * Since: 0.16
 **/
PopplerPermissions poppler_document_get_permissions(PopplerDocument *document)
{
    guint flag = 0;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PERMISSIONS_FULL);

    if (document->doc->okToPrint()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_PRINT;
    }
    if (document->doc->okToChange()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_MODIFY;
    }
    if (document->doc->okToCopy()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_COPY;
    }
    if (document->doc->okToAddNotes()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_ADD_NOTES;
    }
    if (document->doc->okToFillForm()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_FILL_FORM;
    }
    if (document->doc->okToAccessibility()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_EXTRACT_CONTENTS;
    }
    if (document->doc->okToAssemble()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_ASSEMBLE;
    }
    if (document->doc->okToPrintHighRes()) {
        flag |= POPPLER_PERMISSIONS_OK_TO_PRINT_HIGH_RESOLUTION;
    }

    return (PopplerPermissions)flag;
}

/**
 * poppler_document_get_pdf_subtype_string:
 * @document: A #PopplerDocument
 *
 * Returns the PDF subtype version of @document as a string.
 *
 * Returns: (transfer full) (nullable): a newly allocated string containing
 * the PDF subtype version of @document, or %NULL
 *
 * Since: 0.70
 **/
gchar *poppler_document_get_pdf_subtype_string(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    std::unique_ptr<GooString> infostring;

    switch (document->doc->getPDFSubtype()) {
    case subtypePDFA:
        infostring = document->doc->getDocInfoStringEntry("GTS_PDFA1Version");
        break;
    case subtypePDFE:
        infostring = document->doc->getDocInfoStringEntry("GTS_PDFEVersion");
        break;
    case subtypePDFUA:
        infostring = document->doc->getDocInfoStringEntry("GTS_PDFUAVersion");
        break;
    case subtypePDFVT:
        infostring = document->doc->getDocInfoStringEntry("GTS_PDFVTVersion");
        break;
    case subtypePDFX:
        infostring = document->doc->getDocInfoStringEntry("GTS_PDFXVersion");
        break;
    case subtypeNone:
    case subtypeNull:
    default: {
        /* nothing */
    }
    }

    return _poppler_goo_string_to_utf8(infostring.get());
}

/**
 * poppler_document_get_pdf_subtype:
 * @document: A #PopplerDocument
 *
 * Returns the subtype of @document as a #PopplerPDFSubtype.
 *
 * Returns: the document's subtype
 *
 * Since: 0.70
 **/
PopplerPDFSubtype poppler_document_get_pdf_subtype(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PDF_SUBTYPE_NONE);

    return convert_pdf_subtype(document->doc->getPDFSubtype());
}

/**
 * poppler_document_get_pdf_part:
 * @document: A #PopplerDocument
 *
 * Returns the part of the conforming standard that the @document adheres to
 * as a #PopplerPDFSubtype.
 *
 * Returns: the document's subtype part
 *
 * Since: 0.70
 **/
PopplerPDFPart poppler_document_get_pdf_part(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PDF_SUBTYPE_PART_NONE);

    return convert_pdf_subtype_part(document->doc->getPDFSubtypePart());
}

/**
 * poppler_document_get_pdf_conformance:
 * @document: A #PopplerDocument
 *
 * Returns the conformance level of the @document as #PopplerPDFConformance.
 *
 * Returns: the document's subtype conformance level
 *
 * Since: 0.70
 **/
PopplerPDFConformance poppler_document_get_pdf_conformance(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), POPPLER_PDF_SUBTYPE_CONF_NONE);

    return convert_pdf_subtype_conformance(document->doc->getPDFSubtypeConformance());
}

/**
 * poppler_document_get_metadata:
 * @document: A #PopplerDocument
 *
 * Returns the XML metadata string of the document
 *
 * Return value: a new allocated string containing the XML
 *               metadata, or %NULL
 *
 * Since: 0.16
 **/
gchar *poppler_document_get_metadata(PopplerDocument *document)
{
    Catalog *catalog;
    gchar *retval = nullptr;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        std::unique_ptr<GooString> s = catalog->readMetadata();

        if (s) {
            retval = g_strdup(s->c_str());
        }
    }

    return retval;
}

/**
 * poppler_document_reset_form:
 * @document: A #PopplerDocument
 * @fields: (element-type utf8) (nullable): list of fields to reset
 * @exclude_fields: whether to reset all fields except those in @fields
 *
 * Resets the form fields specified by fields if exclude_fields is FALSE.
 * Resets all others if exclude_fields is TRUE.
 * All form fields are reset regardless of the exclude_fields flag
 * if fields is empty.
 *
 * Since: 0.90
 **/
void poppler_document_reset_form(PopplerDocument *document, GList *fields, gboolean exclude_fields)
{
    std::vector<std::string> list;
    Catalog *catalog;
    GList *iter;
    Form *form;

    g_return_if_fail(POPPLER_IS_DOCUMENT(document));

    catalog = document->doc->getCatalog();
    if (catalog && catalog->isOk()) {
        form = catalog->getForm();

        if (form) {
            for (iter = fields; iter != nullptr; iter = iter->next) {
                list.emplace_back(std::string((char *)iter->data));
            }

            form->reset(list, exclude_fields);
        }
    }
}

/**
 * poppler_document_has_javascript:
 * @document: A #PopplerDocument
 *
 * Returns whether @document has any javascript in it.
 *
 * Since: 0.90
 **/
gboolean poppler_document_has_javascript(PopplerDocument *document)
{
    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), FALSE);

    return document->doc->hasJavascript();
}

static void poppler_document_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    PopplerDocument *document = POPPLER_DOCUMENT(object);
    guint version;

    switch (prop_id) {
    case PROP_TITLE:
        g_value_take_string(value, poppler_document_get_title(document));
        break;
    case PROP_FORMAT:
        g_value_take_string(value, poppler_document_get_pdf_version_string(document));
        break;
    case PROP_FORMAT_MAJOR:
        poppler_document_get_pdf_version(document, &version, nullptr);
        g_value_set_uint(value, version);
        break;
    case PROP_FORMAT_MINOR:
        poppler_document_get_pdf_version(document, nullptr, &version);
        g_value_set_uint(value, version);
        break;
    case PROP_AUTHOR:
        g_value_take_string(value, poppler_document_get_author(document));
        break;
    case PROP_SUBJECT:
        g_value_take_string(value, poppler_document_get_subject(document));
        break;
    case PROP_KEYWORDS:
        g_value_take_string(value, poppler_document_get_keywords(document));
        break;
    case PROP_CREATOR:
        g_value_take_string(value, poppler_document_get_creator(document));
        break;
    case PROP_PRODUCER:
        g_value_take_string(value, poppler_document_get_producer(document));
        break;
    case PROP_CREATION_DATE:
        g_value_set_int(value, poppler_document_get_creation_date(document));
        break;
    case PROP_CREATION_DATETIME:
        g_value_take_boxed(value, poppler_document_get_creation_date_time(document));
        break;
    case PROP_MOD_DATE:
        g_value_set_int(value, poppler_document_get_modification_date(document));
        break;
    case PROP_MOD_DATETIME:
        g_value_take_boxed(value, poppler_document_get_modification_date_time(document));
        break;
    case PROP_LINEARIZED:
        g_value_set_boolean(value, poppler_document_is_linearized(document));
        break;
    case PROP_PAGE_LAYOUT:
        g_value_set_enum(value, poppler_document_get_page_layout(document));
        break;
    case PROP_PAGE_MODE:
        g_value_set_enum(value, poppler_document_get_page_mode(document));
        break;
    case PROP_VIEWER_PREFERENCES:
        /* FIXME: write... */
        g_value_set_flags(value, POPPLER_VIEWER_PREFERENCES_UNSET);
        break;
    case PROP_PRINT_SCALING:
        g_value_set_enum(value, poppler_document_get_print_scaling(document));
        break;
    case PROP_PRINT_DUPLEX:
        g_value_set_enum(value, poppler_document_get_print_duplex(document));
        break;
    case PROP_PRINT_N_COPIES:
        g_value_set_int(value, poppler_document_get_print_n_copies(document));
        break;
    case PROP_PERMISSIONS:
        g_value_set_flags(value, poppler_document_get_permissions(document));
        break;
    case PROP_SUBTYPE:
        g_value_set_enum(value, poppler_document_get_pdf_subtype(document));
        break;
    case PROP_SUBTYPE_STRING:
        g_value_take_string(value, poppler_document_get_pdf_subtype_string(document));
        break;
    case PROP_SUBTYPE_PART:
        g_value_set_enum(value, poppler_document_get_pdf_part(document));
        break;
    case PROP_SUBTYPE_CONF:
        g_value_set_enum(value, poppler_document_get_pdf_conformance(document));
        break;
    case PROP_METADATA:
        g_value_take_string(value, poppler_document_get_metadata(document));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void poppler_document_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    PopplerDocument *document = POPPLER_DOCUMENT(object);

    switch (prop_id) {
    case PROP_TITLE:
        poppler_document_set_title(document, g_value_get_string(value));
        break;
    case PROP_AUTHOR:
        poppler_document_set_author(document, g_value_get_string(value));
        break;
    case PROP_SUBJECT:
        poppler_document_set_subject(document, g_value_get_string(value));
        break;
    case PROP_KEYWORDS:
        poppler_document_set_keywords(document, g_value_get_string(value));
        break;
    case PROP_CREATOR:
        poppler_document_set_creator(document, g_value_get_string(value));
        break;
    case PROP_PRODUCER:
        poppler_document_set_producer(document, g_value_get_string(value));
        break;
    case PROP_CREATION_DATE:
        poppler_document_set_creation_date(document, g_value_get_int(value));
        break;
    case PROP_CREATION_DATETIME:
        poppler_document_set_creation_date_time(document, (GDateTime *)g_value_get_boxed(value));
        break;
    case PROP_MOD_DATE:
        poppler_document_set_modification_date(document, g_value_get_int(value));
        break;
    case PROP_MOD_DATETIME:
        poppler_document_set_modification_date_time(document, (GDateTime *)g_value_get_boxed(value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void poppler_document_class_init(PopplerDocumentClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = poppler_document_finalize;
    gobject_class->get_property = poppler_document_get_property;
    gobject_class->set_property = poppler_document_set_property;

    /**
     * PopplerDocument:title:
     *
     * The document's title or %NULL
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_TITLE, g_param_spec_string("title", "Document Title", "The title of the document", nullptr, G_PARAM_READWRITE));

    /**
     * PopplerDocument:format:
     *
     * The PDF version as string. See also poppler_document_get_pdf_version_string()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_FORMAT, g_param_spec_string("format", "PDF Format", "The PDF version of the document", nullptr, G_PARAM_READABLE));

    /**
     * PopplerDocument:format-major:
     *
     * The PDF major version number. See also poppler_document_get_pdf_version()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_FORMAT_MAJOR, g_param_spec_uint("format-major", "PDF Format Major", "The PDF major version number of the document", 0, G_MAXUINT, 1, G_PARAM_READABLE));

    /**
     * PopplerDocument:format-minor:
     *
     * The PDF minor version number. See also poppler_document_get_pdf_version()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_FORMAT_MINOR, g_param_spec_uint("format-minor", "PDF Format Minor", "The PDF minor version number of the document", 0, G_MAXUINT, 0, G_PARAM_READABLE));

    /**
     * PopplerDocument:author:
     *
     * The author of the document
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_AUTHOR, g_param_spec_string("author", "Author", "The author of the document", nullptr, G_PARAM_READWRITE));

    /**
     * PopplerDocument:subject:
     *
     * The subject of the document
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_SUBJECT, g_param_spec_string("subject", "Subject", "Subjects the document touches", nullptr, G_PARAM_READWRITE));

    /**
     * PopplerDocument:keywords:
     *
     * The keywords associated to the document
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_KEYWORDS, g_param_spec_string("keywords", "Keywords", "Keywords", nullptr, G_PARAM_READWRITE));

    /**
     * PopplerDocument:creator:
     *
     * The creator of the document. See also poppler_document_get_creator()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_CREATOR, g_param_spec_string("creator", "Creator", "The software that created the document", nullptr, G_PARAM_READWRITE));

    /**
     * PopplerDocument:producer:
     *
     * The producer of the document. See also poppler_document_get_producer()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_PRODUCER, g_param_spec_string("producer", "Producer", "The software that converted the document", nullptr, G_PARAM_READWRITE));

    /**
     * PopplerDocument:creation-date:
     *
     * The date the document was created as seconds since the Epoch, or -1
     *
     * Deprecated: 20.09.0: This will overflow in 2038. Use creation-datetime
     * instead.
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_CREATION_DATE,
                                    g_param_spec_int("creation-date", "Creation Date", "The date and time the document was created", -1, G_MAXINT, -1, (GParamFlags)(G_PARAM_READWRITE | G_PARAM_DEPRECATED)));

    /**
     * PopplerDocument:creation-datetime:
     * The #GDateTime the document was created.
     *
     * Since: 20.09.0
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_CREATION_DATETIME, g_param_spec_boxed("creation-datetime", "Creation DateTime", "The date and time the document was created", G_TYPE_DATE_TIME, G_PARAM_READWRITE));

    /**
     * PopplerDocument:mod-date:
     *
     * The date the document was most recently modified as seconds since the Epoch, or -1
     *
     * Deprecated: 20.09.0: This will overflow in 2038. Use mod-datetime instead.
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_MOD_DATE,
                                    g_param_spec_int("mod-date", "Modification Date", "The date and time the document was modified", -1, G_MAXINT, -1, (GParamFlags)(G_PARAM_READWRITE | G_PARAM_DEPRECATED)));

    /**
     * PopplerDocument:mod-datetime:
     *
     * The #GDateTime the document was most recently modified.
     *
     * Since: 20.09.0
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_MOD_DATETIME, g_param_spec_boxed("mod-datetime", "Modification DateTime", "The date and time the document was modified", G_TYPE_DATE_TIME, G_PARAM_READWRITE));

    /**
     * PopplerDocument:linearized:
     *
     * Whether document is linearized. See also poppler_document_is_linearized()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_LINEARIZED, g_param_spec_boolean("linearized", "Fast Web View Enabled", "Is the document optimized for web viewing?", FALSE, G_PARAM_READABLE));

    /**
     * PopplerDocument:page-layout:
     *
     * The page layout that should be used when the document is opened
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_PAGE_LAYOUT, g_param_spec_enum("page-layout", "Page Layout", "Initial Page Layout", POPPLER_TYPE_PAGE_LAYOUT, POPPLER_PAGE_LAYOUT_UNSET, G_PARAM_READABLE));

    /**
     * PopplerDocument:page-mode:
     *
     * The mode that should be used when the document is opened
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_PAGE_MODE, g_param_spec_enum("page-mode", "Page Mode", "Page Mode", POPPLER_TYPE_PAGE_MODE, POPPLER_PAGE_MODE_UNSET, G_PARAM_READABLE));

    /**
     * PopplerDocument:viewer-preferences:
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_VIEWER_PREFERENCES,
                                    g_param_spec_flags("viewer-preferences", "Viewer Preferences", "Viewer Preferences", POPPLER_TYPE_VIEWER_PREFERENCES, POPPLER_VIEWER_PREFERENCES_UNSET, G_PARAM_READABLE));

    /**
     * PopplerDocument:print-scaling:
     *
     * Since: 0.73
     */
    g_object_class_install_property(
            G_OBJECT_CLASS(klass), PROP_PRINT_SCALING,
            g_param_spec_enum("print-scaling", "Print Scaling", "Print Scaling Viewer Preference", POPPLER_TYPE_PRINT_SCALING, POPPLER_PRINT_SCALING_APP_DEFAULT, (GParamFlags)(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));

    /**
     * PopplerDocument:print-duplex:
     *
     * Since: 0.80
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_PRINT_DUPLEX,
                                    g_param_spec_enum("print-duplex", "Print Duplex", "Duplex Viewer Preference", POPPLER_TYPE_PRINT_DUPLEX, POPPLER_PRINT_DUPLEX_NONE, (GParamFlags)(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));

    /**
     * PopplerDocument:print-n-copies:
     *
     * Suggested number of copies to be printed for this document
     *
     * Since: 0.80
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_PRINT_N_COPIES,
                                    g_param_spec_int("print-n-copies", "Number of Copies to Print", "Number of Copies Viewer Preference", 1, G_MAXINT, 1, (GParamFlags)(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));

    /**
     * PopplerDocument:permissions:
     *
     * Flags specifying which operations are permitted when the document is opened
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_PERMISSIONS, g_param_spec_flags("permissions", "Permissions", "Permissions", POPPLER_TYPE_PERMISSIONS, POPPLER_PERMISSIONS_FULL, G_PARAM_READABLE));

    /**
     * PopplerDocument:subtype:
     *
     *  Document PDF subtype type
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_SUBTYPE, g_param_spec_enum("subtype", "PDF Format Subtype Type", "The PDF subtype of the document", POPPLER_TYPE_PDF_SUBTYPE, POPPLER_PDF_SUBTYPE_UNSET, G_PARAM_READABLE));

    /**
     * PopplerDocument:subtype-string:
     *
     *  Document PDF subtype. See also poppler_document_get_pdf_subtype_string()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_SUBTYPE_STRING, g_param_spec_string("subtype-string", "PDF Format Subtype", "The PDF subtype of the document", nullptr, G_PARAM_READABLE));

    /**
     * PopplerDocument:subtype-part:
     *
     *  Document PDF subtype part
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_SUBTYPE_PART,
                                    g_param_spec_enum("subtype-part", "PDF Format Subtype Part", "The part of PDF conformance", POPPLER_TYPE_PDF_PART, POPPLER_PDF_SUBTYPE_PART_UNSET, G_PARAM_READABLE));

    /**
     * PopplerDocument:subtype-conformance:
     *
     *  Document PDF subtype conformance
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_SUBTYPE_CONF,
                                    g_param_spec_enum("subtype-conformance", "PDF Format Subtype Conformance", "The conformance level of PDF subtype", POPPLER_TYPE_PDF_CONFORMANCE, POPPLER_PDF_SUBTYPE_CONF_UNSET, G_PARAM_READABLE));

    /**
     * PopplerDocument:metadata:
     *
     * Document metadata in XML format, or %NULL
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_METADATA, g_param_spec_string("metadata", "XML Metadata", "Embedded XML metadata", nullptr, G_PARAM_READABLE));
}

static void poppler_document_init(PopplerDocument *document) { }

/* PopplerIndexIter: For determining the index of a tree */
struct _PopplerIndexIter
{
    PopplerDocument *document;
    const std::vector<OutlineItem *> *items;
    int index;
};

G_DEFINE_BOXED_TYPE(PopplerIndexIter, poppler_index_iter, poppler_index_iter_copy, poppler_index_iter_free)

/**
 * poppler_index_iter_copy:
 * @iter: a #PopplerIndexIter
 *
 * Creates a new #PopplerIndexIter as a copy of @iter.  This must be freed with
 * poppler_index_iter_free().
 *
 * Return value: a new #PopplerIndexIter
 **/
PopplerIndexIter *poppler_index_iter_copy(PopplerIndexIter *iter)
{
    PopplerIndexIter *new_iter;

    g_return_val_if_fail(iter != nullptr, NULL);

    new_iter = g_slice_dup(PopplerIndexIter, iter);
    new_iter->document = (PopplerDocument *)g_object_ref(new_iter->document);

    return new_iter;
}

/**
 * poppler_index_iter_new:
 * @document: a #PopplerDocument
 *
 * Returns the root #PopplerIndexIter for @document, or %NULL.  This must be
 * freed with poppler_index_iter_free().
 *
 * Certain documents have an index associated with them.  This index can be used
 * to help the user navigate the document, and is similar to a table of
 * contents.  Each node in the index will contain a #PopplerAction that can be
 * displayed to the user &mdash; typically a #POPPLER_ACTION_GOTO_DEST or a
 * #POPPLER_ACTION_URI<!-- -->.
 *
 * Here is a simple example of some code that walks the full index:
 *
 * <informalexample><programlisting>
 * static void
 * walk_index (PopplerIndexIter *iter)
 * {
 *   do
 *     {
 *       /<!-- -->* Get the action and do something with it *<!-- -->/
 *       PopplerIndexIter *child = poppler_index_iter_get_child (iter);
 *       if (child)
 *         walk_index (child);
 *       poppler_index_iter_free (child);
 *     }
 *   while (poppler_index_iter_next (iter));
 * }
 * ...
 * {
 *   iter = poppler_index_iter_new (document);
 *   walk_index (iter);
 *   poppler_index_iter_free (iter);
 * }
 *</programlisting></informalexample>
 *
 * Return value: a new #PopplerIndexIter
 **/
PopplerIndexIter *poppler_index_iter_new(PopplerDocument *document)
{
    PopplerIndexIter *iter;
    Outline *outline;
    const std::vector<OutlineItem *> *items;

    outline = document->doc->getOutline();
    if (outline == nullptr) {
        return nullptr;
    }

    items = outline->getItems();
    if (items == nullptr) {
        return nullptr;
    }

    iter = g_slice_new(PopplerIndexIter);
    iter->document = (PopplerDocument *)g_object_ref(document);
    iter->items = items;
    iter->index = 0;

    return iter;
}

/**
 * poppler_index_iter_get_child:
 * @parent: a #PopplerIndexIter
 *
 * Returns a newly created child of @parent, or %NULL if the iter has no child.
 * See poppler_index_iter_new() for more information on this function.
 *
 * Return value: a new #PopplerIndexIter
 **/
PopplerIndexIter *poppler_index_iter_get_child(PopplerIndexIter *parent)
{
    PopplerIndexIter *child;
    OutlineItem *item;

    g_return_val_if_fail(parent != nullptr, NULL);

    item = (*parent->items)[parent->index];
    item->open();
    if (!(item->hasKids() && item->getKids())) {
        return nullptr;
    }

    child = g_slice_new0(PopplerIndexIter);
    child->document = (PopplerDocument *)g_object_ref(parent->document);
    child->items = item->getKids();

    g_assert(child->items);

    return child;
}

static gchar *unicode_to_char(const Unicode *unicode, int len)
{
    const UnicodeMap *uMap = globalParams->getUtf8Map();

    GooString gstr;
    gchar buf[8]; /* 8 is enough for mapping an unicode char to a string */
    int i, n;

    for (i = 0; i < len; ++i) {
        n = uMap->mapUnicode(unicode[i], buf, sizeof(buf));
        gstr.append(buf, n);
    }

    return g_strdup(gstr.c_str());
}

/**
 * poppler_index_iter_is_open:
 * @iter: a #PopplerIndexIter
 *
 * Returns whether this node should be expanded by default to the user.  The
 * document can provide a hint as to how the document's index should be expanded
 * initially.
 *
 * Return value: %TRUE, if the document wants @iter to be expanded
 **/
gboolean poppler_index_iter_is_open(PopplerIndexIter *iter)
{
    OutlineItem *item;

    item = (*iter->items)[iter->index];

    return item->isOpen();
}

/**
 * poppler_index_iter_get_action:
 * @iter: a #PopplerIndexIter
 *
 * Returns the #PopplerAction associated with @iter.  It must be freed with
 * poppler_action_free().
 *
 * Return value: a new #PopplerAction
 **/
PopplerAction *poppler_index_iter_get_action(PopplerIndexIter *iter)
{
    OutlineItem *item;
    const LinkAction *link_action;
    PopplerAction *action;
    gchar *title;

    g_return_val_if_fail(iter != nullptr, NULL);

    item = (*iter->items)[iter->index];
    link_action = item->getAction();

    title = unicode_to_char(item->getTitle(), item->getTitleLength());

    action = _poppler_action_new(iter->document, link_action, title);
    g_free(title);

    return action;
}

/**
 * poppler_index_iter_next:
 * @iter: a #PopplerIndexIter
 *
 * Sets @iter to point to the next action at the current level, if valid.  See
 * poppler_index_iter_new() for more information.
 *
 * Return value: %TRUE, if @iter was set to the next action
 **/
gboolean poppler_index_iter_next(PopplerIndexIter *iter)
{
    g_return_val_if_fail(iter != nullptr, FALSE);

    iter->index++;
    if (iter->index >= (int)iter->items->size()) {
        return FALSE;
    }

    return TRUE;
}

/**
 * poppler_index_iter_free:
 * @iter: a #PopplerIndexIter
 *
 * Frees @iter.
 **/
void poppler_index_iter_free(PopplerIndexIter *iter)
{
    if (G_UNLIKELY(iter == nullptr)) {
        return;
    }

    g_object_unref(iter->document);
    g_slice_free(PopplerIndexIter, iter);
}

struct _PopplerFontsIter
{
    std::vector<FontInfo *> items;
    int index;
};

G_DEFINE_BOXED_TYPE(PopplerFontsIter, poppler_fonts_iter, poppler_fonts_iter_copy, poppler_fonts_iter_free)

/**
 * poppler_fonts_iter_get_full_name:
 * @iter: a #PopplerFontsIter
 *
 * Returns the full name of the font associated with @iter
 *
 * Returns: the font full name
 */
const char *poppler_fonts_iter_get_full_name(PopplerFontsIter *iter)
{
    FontInfo *info;

    info = iter->items[iter->index];

    const std::optional<std::string> &name = info->getName();
    if (name) {
        return name->c_str();
    } else {
        return nullptr;
    }
}

/**
 * poppler_fonts_iter_get_name:
 * @iter: a #PopplerFontsIter
 *
 * Returns the name of the font associated with @iter
 *
 * Returns: the font name
 */
const char *poppler_fonts_iter_get_name(PopplerFontsIter *iter)
{
    FontInfo *info;
    const char *name;

    name = poppler_fonts_iter_get_full_name(iter);
    info = iter->items[iter->index];

    if (info->getSubset() && name) {
        while (*name && *name != '+') {
            name++;
        }

        if (*name) {
            name++;
        }
    }

    return name;
}

/**
 * poppler_fonts_iter_get_substitute_name:
 * @iter: a #PopplerFontsIter
 *
 * The name of the substitute font of the font associated with @iter or %NULL if
 * the font is embedded
 *
 * Returns: the name of the substitute font or %NULL if font is embedded
 *
 * Since: 0.20
 */
const char *poppler_fonts_iter_get_substitute_name(PopplerFontsIter *iter)
{
    FontInfo *info;

    info = iter->items[iter->index];

    const std::optional<std::string> &name = info->getSubstituteName();
    if (name) {
        return name->c_str();
    } else {
        return nullptr;
    }
}

/**
 * poppler_fonts_iter_get_file_name:
 * @iter: a #PopplerFontsIter
 *
 * The filename of the font associated with @iter or %NULL if
 * the font is embedded
 *
 * Returns: the filename of the font or %NULL if font is embedded
 */
const char *poppler_fonts_iter_get_file_name(PopplerFontsIter *iter)
{
    FontInfo *info;

    info = iter->items[iter->index];

    const std::optional<std::string> &file = info->getFile();
    if (file) {
        return file->c_str();
    } else {
        return nullptr;
    }
}

/**
 * poppler_fonts_iter_get_font_type:
 * @iter: a #PopplerFontsIter
 *
 * Returns the type of the font associated with @iter
 *
 * Returns: the font type
 */
PopplerFontType poppler_fonts_iter_get_font_type(PopplerFontsIter *iter)
{
    FontInfo *info;

    g_return_val_if_fail(iter != nullptr, POPPLER_FONT_TYPE_UNKNOWN);

    info = iter->items[iter->index];

    return (PopplerFontType)info->getType();
}

/**
 * poppler_fonts_iter_get_encoding:
 * @iter: a #PopplerFontsIter
 *
 * Returns the encoding of the font associated with @iter
 *
 * Returns: the font encoding
 *
 * Since: 0.20
 */
const char *poppler_fonts_iter_get_encoding(PopplerFontsIter *iter)
{
    FontInfo *info;

    info = iter->items[iter->index];

    const std::string &encoding = info->getEncoding();
    if (!encoding.empty()) {
        return encoding.c_str();
    } else {
        return nullptr;
    }
}

/**
 * poppler_fonts_iter_is_embedded:
 * @iter: a #PopplerFontsIter
 *
 * Returns whether the font associated with @iter is embedded in the document
 *
 * Returns: %TRUE if font is embedded, %FALSE otherwise
 */
gboolean poppler_fonts_iter_is_embedded(PopplerFontsIter *iter)
{
    FontInfo *info;

    info = iter->items[iter->index];

    return info->getEmbedded();
}

/**
 * poppler_fonts_iter_is_subset:
 * @iter: a #PopplerFontsIter
 *
 * Returns whether the font associated with @iter is a subset of another font
 *
 * Returns: %TRUE if font is a subset, %FALSE otherwise
 */
gboolean poppler_fonts_iter_is_subset(PopplerFontsIter *iter)
{
    FontInfo *info;

    info = iter->items[iter->index];

    return info->getSubset();
}

/**
 * poppler_fonts_iter_next:
 * @iter: a #PopplerFontsIter
 *
 * Sets @iter to point to the next font
 *
 * Returns: %TRUE, if @iter was set to the next font
 **/
gboolean poppler_fonts_iter_next(PopplerFontsIter *iter)
{
    g_return_val_if_fail(iter != nullptr, FALSE);

    iter->index++;
    if (iter->index >= (int)iter->items.size()) {
        return FALSE;
    }

    return TRUE;
}

/**
 * poppler_fonts_iter_copy:
 * @iter: a #PopplerFontsIter to copy
 *
 * Creates a copy of @iter
 *
 * Returns: a new allocated copy of @iter
 */
PopplerFontsIter *poppler_fonts_iter_copy(PopplerFontsIter *iter)
{
    PopplerFontsIter *new_iter;

    g_return_val_if_fail(iter != nullptr, NULL);

    new_iter = g_slice_dup(PopplerFontsIter, iter);

    new_iter->items.resize(iter->items.size());
    for (std::size_t i = 0; i < iter->items.size(); i++) {
        FontInfo *info = iter->items[i];
        new_iter->items[i] = new FontInfo(*info);
    }

    return new_iter;
}

/**
 * poppler_fonts_iter_free:
 * @iter: a #PopplerFontsIter
 *
 * Frees the given #PopplerFontsIter
 */
void poppler_fonts_iter_free(PopplerFontsIter *iter)
{
    if (G_UNLIKELY(iter == nullptr)) {
        return;
    }

    for (auto entry : iter->items) {
        delete entry;
    }
    iter->items.~vector<FontInfo *>();

    g_slice_free(PopplerFontsIter, iter);
}

static PopplerFontsIter *poppler_fonts_iter_new(std::vector<FontInfo *> &&items)
{
    PopplerFontsIter *iter;

    iter = g_slice_new(PopplerFontsIter);
    new ((void *)&iter->items) std::vector<FontInfo *>(std::move(items));
    iter->index = 0;

    return iter;
}

typedef struct _PopplerFontInfoClass PopplerFontInfoClass;
struct _PopplerFontInfoClass
{
    GObjectClass parent_class;
};

G_DEFINE_TYPE(PopplerFontInfo, poppler_font_info, G_TYPE_OBJECT)

static void poppler_font_info_finalize(GObject *object);

static void poppler_font_info_class_init(PopplerFontInfoClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = poppler_font_info_finalize;
}

static void poppler_font_info_init(PopplerFontInfo *font_info)
{
    font_info->document = nullptr;
    font_info->scanner = nullptr;
}

static void poppler_font_info_finalize(GObject *object)
{
    PopplerFontInfo *font_info = POPPLER_FONT_INFO(object);

    delete font_info->scanner;
    g_object_unref(font_info->document);

    G_OBJECT_CLASS(poppler_font_info_parent_class)->finalize(object);
}

/**
 * poppler_font_info_new:
 * @document: a #PopplerDocument
 *
 * Creates a new #PopplerFontInfo object
 *
 * Returns: a new #PopplerFontInfo instance
 */
PopplerFontInfo *poppler_font_info_new(PopplerDocument *document)
{
    PopplerFontInfo *font_info;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    font_info = (PopplerFontInfo *)g_object_new(POPPLER_TYPE_FONT_INFO, nullptr);
    font_info->document = (PopplerDocument *)g_object_ref(document);
    font_info->scanner = new FontInfoScanner(document->doc);

    return font_info;
}

/**
 * poppler_font_info_scan:
 * @font_info: a #PopplerFontInfo
 * @n_pages: number of pages to scan
 * @iter: (out): return location for a #PopplerFontsIter
 *
 * Scans the document associated with @font_info for fonts. At most
 * @n_pages will be scanned starting from the current iterator. @iter will
 * point to the first font scanned.
 *
 * Here is a simple example of code to scan fonts in a document
 *
 * <informalexample><programlisting>
 * font_info = poppler_font_info_new (document);
 * while (poppler_font_info_scan (font_info, 20, &fonts_iter)) {
 *         if (!fonts_iter)
 *                 continue; /<!-- -->* No fonts found in these 20 pages *<!-- -->/
 *         do {
 *                 /<!-- -->* Do something with font iter *<!-- -->/
 *                 g_print ("Font Name: %s\n", poppler_fonts_iter_get_name (fonts_iter));
 *         } while (poppler_fonts_iter_next (fonts_iter));
 *         poppler_fonts_iter_free (fonts_iter);
 * }
 * </programlisting></informalexample>
 *
 * Returns: %TRUE, if there are more fonts left to scan
 */
gboolean poppler_font_info_scan(PopplerFontInfo *font_info, int n_pages, PopplerFontsIter **iter)
{
    g_return_val_if_fail(iter != nullptr, FALSE);

    std::vector<FontInfo *> items = font_info->scanner->scan(n_pages);

    if (items.empty()) {
        *iter = nullptr;
        return FALSE;
    } else {
        *iter = poppler_fonts_iter_new(std::move(items));
    }

    return TRUE;
}

/* For backward compatibility */
void poppler_font_info_free(PopplerFontInfo *font_info)
{
    g_return_if_fail(font_info != nullptr);

    g_object_unref(font_info);
}

/* Optional content (layers) */
static Layer *layer_new(OptionalContentGroup *oc)
{
    Layer *layer;

    layer = g_slice_new0(Layer);
    layer->oc = oc;

    return layer;
}

static void layer_free(Layer *layer)
{
    if (G_UNLIKELY(!layer)) {
        return;
    }

    if (layer->kids) {
        g_list_free_full(layer->kids, (GDestroyNotify)layer_free);
    }

    if (layer->label) {
        g_free(layer->label);
    }

    g_slice_free(Layer, layer);
}

static GList *get_optional_content_rbgroups(OCGs *ocg)
{
    Array *rb;
    GList *groups = nullptr;

    rb = ocg->getRBGroupsArray();

    if (rb) {
        int i, j;

        for (i = 0; i < rb->getLength(); ++i) {
            Array *rb_array;
            GList *group = nullptr;

            Object obj = rb->get(i);
            if (!obj.isArray()) {
                continue;
            }

            rb_array = obj.getArray();
            for (j = 0; j < rb_array->getLength(); ++j) {
                OptionalContentGroup *oc;

                const Object &ref = rb_array->getNF(j);
                if (!ref.isRef()) {
                    continue;
                }

                oc = ocg->findOcgByRef(ref.getRef());
                group = g_list_prepend(group, oc);
            }

            groups = g_list_prepend(groups, group);
        }
    }

    return groups;
}

GList *_poppler_document_get_layer_rbgroup(PopplerDocument *document, Layer *layer)
{
    GList *l;

    for (l = document->layers_rbgroups; l && l->data; l = g_list_next(l)) {
        GList *group = (GList *)l->data;

        if (g_list_find(group, layer->oc)) {
            return group;
        }
    }

    return nullptr;
}

static GList *get_optional_content_items_sorted(OCGs *ocg, Layer *parent, Array *order)
{
    GList *items = nullptr;
    Layer *last_item = parent;
    int i;

    for (i = 0; i < order->getLength(); ++i) {
        Object orderItem = order->get(i);

        if (orderItem.isDict()) {
            const Object &ref = order->getNF(i);
            if (ref.isRef()) {
                OptionalContentGroup *oc = ocg->findOcgByRef(ref.getRef());
                Layer *layer = layer_new(oc);

                items = g_list_prepend(items, layer);
                last_item = layer;
            }
        } else if (orderItem.isArray() && orderItem.arrayGetLength() > 0) {
            if (!last_item) {
                last_item = layer_new(nullptr);
                items = g_list_prepend(items, last_item);
            }
            last_item->kids = get_optional_content_items_sorted(ocg, last_item, orderItem.getArray());
            last_item = nullptr;
        } else if (orderItem.isString()) {
            last_item->label = _poppler_goo_string_to_utf8(orderItem.getString());
        }
    }

    return g_list_reverse(items);
}

static GList *get_optional_content_items(OCGs *ocg)
{
    Array *order;
    GList *items = nullptr;

    order = ocg->getOrderArray();

    if (order) {
        items = get_optional_content_items_sorted(ocg, nullptr, order);
    } else {
        const auto &ocgs = ocg->getOCGs();

        for (const auto &oc : ocgs) {
            Layer *layer = layer_new(oc.second.get());

            items = g_list_prepend(items, layer);
        }

        items = g_list_reverse(items);
    }

    return items;
}

GList *_poppler_document_get_layers(PopplerDocument *document)
{
    if (!document->layers) {
        Catalog *catalog = document->doc->getCatalog();
        OCGs *ocg = catalog->getOptContentConfig();

        if (!ocg) {
            return nullptr;
        }

        document->layers = get_optional_content_items(ocg);
        document->layers_rbgroups = get_optional_content_rbgroups(ocg);
    }

    return document->layers;
}

static void poppler_document_layers_free(PopplerDocument *document)
{
    if (G_UNLIKELY(!document->layers)) {
        return;
    }

    g_list_free_full(document->layers, (GDestroyNotify)layer_free);
    g_list_free_full(document->layers_rbgroups, (GDestroyNotify)g_list_free);

    document->layers = nullptr;
    document->layers_rbgroups = nullptr;
}

/* PopplerLayersIter */
struct _PopplerLayersIter
{
    PopplerDocument *document;
    GList *items;
    int index;
};

G_DEFINE_BOXED_TYPE(PopplerLayersIter, poppler_layers_iter, poppler_layers_iter_copy, poppler_layers_iter_free)

/**
 * poppler_layers_iter_copy:
 * @iter: a #PopplerLayersIter
 *
 * Creates a new #PopplerLayersIter as a copy of @iter.  This must be freed with
 * poppler_layers_iter_free().
 *
 * Return value: a new #PopplerLayersIter
 *
 * Since 0.12
 **/
PopplerLayersIter *poppler_layers_iter_copy(PopplerLayersIter *iter)
{
    PopplerLayersIter *new_iter;

    g_return_val_if_fail(iter != nullptr, NULL);

    new_iter = g_slice_dup(PopplerLayersIter, iter);
    new_iter->document = (PopplerDocument *)g_object_ref(new_iter->document);

    return new_iter;
}

/**
 * poppler_layers_iter_free:
 * @iter: a #PopplerLayersIter
 *
 * Frees @iter.
 *
 * Since: 0.12
 **/
void poppler_layers_iter_free(PopplerLayersIter *iter)
{
    if (G_UNLIKELY(iter == nullptr)) {
        return;
    }

    g_object_unref(iter->document);
    g_slice_free(PopplerLayersIter, iter);
}

/**
 * poppler_layers_iter_new:
 * @document: a #PopplerDocument
 *
 * Since: 0.12
 **/
PopplerLayersIter *poppler_layers_iter_new(PopplerDocument *document)
{
    PopplerLayersIter *iter;
    GList *items;

    items = _poppler_document_get_layers(document);

    if (!items) {
        return nullptr;
    }

    iter = g_slice_new0(PopplerLayersIter);
    iter->document = (PopplerDocument *)g_object_ref(document);
    iter->items = items;

    return iter;
}

/**
 * poppler_layers_iter_get_child:
 * @parent: a #PopplerLayersIter
 *
 * Returns a newly created child of @parent, or %NULL if the iter has no child.
 * See poppler_layers_iter_new() for more information on this function.
 *
 * Return value: a new #PopplerLayersIter, or %NULL
 *
 * Since: 0.12
 **/
PopplerLayersIter *poppler_layers_iter_get_child(PopplerLayersIter *parent)
{
    PopplerLayersIter *child;
    Layer *layer;

    g_return_val_if_fail(parent != nullptr, NULL);

    layer = (Layer *)g_list_nth_data(parent->items, parent->index);
    if (!layer || !layer->kids) {
        return nullptr;
    }

    child = g_slice_new0(PopplerLayersIter);
    child->document = (PopplerDocument *)g_object_ref(parent->document);
    child->items = layer->kids;

    g_assert(child->items);

    return child;
}

/**
 * poppler_layers_iter_get_title:
 * @iter: a #PopplerLayersIter
 *
 * Returns the title associated with @iter.  It must be freed with
 * g_free().
 *
 * Return value: a new string containing the @iter's title or %NULL if @iter doesn't have a title.
 * The returned string should be freed with g_free() when no longer needed.
 *
 * Since: 0.12
 **/
gchar *poppler_layers_iter_get_title(PopplerLayersIter *iter)
{
    Layer *layer;

    g_return_val_if_fail(iter != nullptr, NULL);

    layer = (Layer *)g_list_nth_data(iter->items, iter->index);

    return layer->label ? g_strdup(layer->label) : nullptr;
}

/**
 * poppler_layers_iter_get_layer:
 * @iter: a #PopplerLayersIter
 *
 * Returns the #PopplerLayer associated with @iter.
 *
 * Return value: (transfer full): a new #PopplerLayer, or %NULL if
 * there isn't any layer associated with @iter
 *
 * Since: 0.12
 **/
PopplerLayer *poppler_layers_iter_get_layer(PopplerLayersIter *iter)
{
    Layer *layer;
    PopplerLayer *poppler_layer = nullptr;

    g_return_val_if_fail(iter != nullptr, NULL);

    layer = (Layer *)g_list_nth_data(iter->items, iter->index);
    if (layer->oc) {
        GList *rb_group = nullptr;

        rb_group = _poppler_document_get_layer_rbgroup(iter->document, layer);
        poppler_layer = _poppler_layer_new(iter->document, layer, rb_group);
    }

    return poppler_layer;
}

/**
 * poppler_layers_iter_next:
 * @iter: a #PopplerLayersIter
 *
 * Sets @iter to point to the next action at the current level, if valid.  See
 * poppler_layers_iter_new() for more information.
 *
 * Return value: %TRUE, if @iter was set to the next action
 *
 * Since: 0.12
 **/
gboolean poppler_layers_iter_next(PopplerLayersIter *iter)
{
    g_return_val_if_fail(iter != nullptr, FALSE);

    iter->index++;
    if (iter->index >= (gint)g_list_length(iter->items)) {
        return FALSE;
    }

    return TRUE;
}

typedef struct _PopplerPSFileClass PopplerPSFileClass;
struct _PopplerPSFileClass
{
    GObjectClass parent_class;
};

G_DEFINE_TYPE(PopplerPSFile, poppler_ps_file, G_TYPE_OBJECT)

static void poppler_ps_file_finalize(GObject *object);

static void poppler_ps_file_class_init(PopplerPSFileClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = poppler_ps_file_finalize;
}

static void poppler_ps_file_init(PopplerPSFile *ps_file)
{
    ps_file->out = nullptr;
    ps_file->fd = -1;
    ps_file->filename = nullptr;
    ps_file->paper_width = -1;
    ps_file->paper_height = -1;
    ps_file->duplex = FALSE;
}

static void poppler_ps_file_finalize(GObject *object)
{
    PopplerPSFile *ps_file = POPPLER_PS_FILE(object);

    delete ps_file->out;
    g_object_unref(ps_file->document);
    g_free(ps_file->filename);
#ifndef G_OS_WIN32
    if (ps_file->fd != -1) {
        close(ps_file->fd);
    }
#endif /* !G_OS_WIN32 */

    G_OBJECT_CLASS(poppler_ps_file_parent_class)->finalize(object);
}

/**
 * poppler_ps_file_new:
 * @document: a #PopplerDocument
 * @filename: the path of the output filename
 * @first_page: the first page to print
 * @n_pages: the number of pages to print
 *
 * Create a new postscript file to render to
 *
 * Return value: (transfer full): a PopplerPSFile
 **/
PopplerPSFile *poppler_ps_file_new(PopplerDocument *document, const char *filename, int first_page, int n_pages)
{
    PopplerPSFile *ps_file;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);
    g_return_val_if_fail(filename != nullptr, NULL);
    g_return_val_if_fail(n_pages > 0, NULL);

    ps_file = (PopplerPSFile *)g_object_new(POPPLER_TYPE_PS_FILE, nullptr);
    ps_file->document = (PopplerDocument *)g_object_ref(document);
    ps_file->filename = g_strdup(filename);
    ps_file->first_page = first_page + 1;
    ps_file->last_page = first_page + 1 + n_pages - 1;

    return ps_file;
}

#ifndef G_OS_WIN32

/**
 * poppler_ps_file_new_fd:
 * @document: a #PopplerDocument
 * @fd: a valid file descriptor open for writing
 * @first_page: the first page to print
 * @n_pages: the number of pages to print
 *
 * Create a new postscript file to render to.
 * Note that this function takes ownership of @fd; you must not operate on it
 * again, nor close it.
 *
 * Return value: (transfer full): a #PopplerPSFile
 *
 * Since: 21.12.0
 **/
PopplerPSFile *poppler_ps_file_new_fd(PopplerDocument *document, int fd, int first_page, int n_pages)
{
    PopplerPSFile *ps_file;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), nullptr);
    g_return_val_if_fail(fd != -1, nullptr);
    g_return_val_if_fail(n_pages > 0, nullptr);

    ps_file = (PopplerPSFile *)g_object_new(POPPLER_TYPE_PS_FILE, nullptr);
    ps_file->document = (PopplerDocument *)g_object_ref(document);
    ps_file->fd = fd;
    ps_file->first_page = first_page + 1;
    ps_file->last_page = first_page + 1 + n_pages - 1;

    return ps_file;
}

#endif /* !G_OS_WIN32 */

/**
 * poppler_ps_file_set_paper_size:
 * @ps_file: a PopplerPSFile which was not yet printed to.
 * @width: the paper width in 1/72 inch
 * @height: the paper height in 1/72 inch
 *
 * Set the output paper size. These values will end up in the
 * DocumentMedia, the BoundingBox DSC comments and other places in the
 * generated PostScript.
 *
 **/
void poppler_ps_file_set_paper_size(PopplerPSFile *ps_file, double width, double height)
{
    g_return_if_fail(ps_file->out == nullptr);

    ps_file->paper_width = width;
    ps_file->paper_height = height;
}

/**
 * poppler_ps_file_set_duplex:
 * @ps_file: a PopplerPSFile which was not yet printed to
 * @duplex: whether to force duplex printing (on printers which support this)
 *
 * Enable or disable Duplex printing.
 *
 **/
void poppler_ps_file_set_duplex(PopplerPSFile *ps_file, gboolean duplex)
{
    g_return_if_fail(ps_file->out == nullptr);

    ps_file->duplex = duplex;
}

/**
 * poppler_ps_file_free:
 * @ps_file: a PopplerPSFile
 *
 * Frees @ps_file
 *
 **/
void poppler_ps_file_free(PopplerPSFile *ps_file)
{
    g_return_if_fail(ps_file != nullptr);
    g_object_unref(ps_file);
}

/**
 * poppler_document_get_form_field:
 * @document: a #PopplerDocument
 * @id: an id of a #PopplerFormField
 *
 * Returns the #PopplerFormField for the given @id. It must be freed with
 * g_object_unref()
 *
 * Return value: (transfer full): a new #PopplerFormField or %NULL if
 * not found
 **/
PopplerFormField *poppler_document_get_form_field(PopplerDocument *document, gint id)
{
    Page *page;
    unsigned pageNum;
    unsigned fieldNum;
    FormWidget *field;

    FormWidget::decodeID(id, &pageNum, &fieldNum);

    page = document->doc->getPage(pageNum);
    if (!page) {
        return nullptr;
    }

    const std::unique_ptr<FormPageWidgets> widgets = page->getFormWidgets();
    if (!widgets) {
        return nullptr;
    }

    field = widgets->getWidget(fieldNum);
    if (field) {
        return _poppler_form_field_new(document, field);
    }

    return nullptr;
}

gboolean _poppler_convert_pdf_date_to_gtime(const GooString *date, time_t *gdate)
{
    gchar *date_string;
    gboolean retval;

    if (date->hasUnicodeMarker()) {
        date_string = g_convert(date->c_str() + 2, date->getLength() - 2, "UTF-8", "UTF-16BE", nullptr, nullptr, nullptr);
    } else {
        date_string = g_strndup(date->c_str(), date->getLength());
    }

    retval = poppler_date_parse(date_string, gdate);
    g_free(date_string);

    return retval;
}

/**
 * _poppler_convert_pdf_date_to_date_time:
 * @date: a PDF date
 *
 * Converts the PDF date in @date to a #GDateTime.
 *
 * Returns: The converted date, or %NULL on error.
 **/
GDateTime *_poppler_convert_pdf_date_to_date_time(const GooString *date)
{
    GDateTime *date_time = nullptr;
    GTimeZone *time_zone = nullptr;
    int year, mon, day, hour, min, sec, tzHours, tzMins;
    char tz;

    if (parseDateString(date, &year, &mon, &day, &hour, &min, &sec, &tz, &tzHours, &tzMins)) {
        if (tz == '+' || tz == '-') {
            gchar *identifier;

            identifier = g_strdup_printf("%c%02u:%02u", tz, tzHours, tzMins);
#if GLIB_CHECK_VERSION(2, 68, 0)
            time_zone = g_time_zone_new_identifier(identifier);
            if (!time_zone) {
                g_debug("Failed to create time zone for identifier \"%s\"", identifier);
                time_zone = g_time_zone_new_utc();
            }
#else
            time_zone = g_time_zone_new(identifier);
#endif
            g_free(identifier);
        } else if (tz == '\0' || tz == 'Z') {
            time_zone = g_time_zone_new_utc();
        } else {
            g_warning("unexpected tz val '%c'", tz);
            time_zone = g_time_zone_new_utc();
        }

        date_time = g_date_time_new(time_zone, year, mon, day, hour, min, sec);
        g_time_zone_unref(time_zone);
    }

    return date_time;
}

/**
 * _poppler_convert_date_time_to_pdf_date:
 * @datetime: a #GDateTime
 *
 * Converts a #GDateTime to a PDF date.
 *
 * Returns: The converted date
 **/
GooString *_poppler_convert_date_time_to_pdf_date(GDateTime *datetime)
{
    int offset_min;
    gchar *date_str;
    std::unique_ptr<GooString> out_str;

    offset_min = g_date_time_get_utc_offset(datetime) / 1000000 / 60;
    date_str = g_date_time_format(datetime, "D:%Y%m%d%H%M%S");

    if (offset_min == 0) {
        out_str = GooString::format("{0:s}Z", date_str);
    } else {
        char tz = offset_min > 0 ? '+' : '-';

        out_str = GooString::format("{0:s}{1:c}{2:02d}'{3:02d}'", date_str, tz, offset_min / 60, offset_min % 60);
    }

    g_free(date_str);
    return out_str.release();
}
