/* poppler-page.cc: glib wrapper for poppler
 * Copyright (C) 2005, Red Hat, Inc.
 *
 * 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 <cmath>

#ifndef __GI_SCANNER__
#    include <GlobalParams.h>
#    include <PDFDoc.h>
#    include <Outline.h>
#    include <ErrorCodes.h>
#    include <UnicodeMap.h>
#    include <GfxState.h>
#    include <PageTransition.h>
#    include <BBoxOutputDev.h>
#endif

#include "poppler.h"
#include "poppler-private.h"

static void _page_unrotate_xy(Page *page, double *x, double *y);

/**
 * SECTION:poppler-page
 * @short_description: Information about a page in a document
 * @title: PopplerPage
 */

enum
{
    PROP_0,
    PROP_LABEL
};

static PopplerRectangleExtended *poppler_rectangle_extended_new();

typedef struct _PopplerPageClass PopplerPageClass;
struct _PopplerPageClass
{
    GObjectClass parent_class;
};

G_DEFINE_TYPE(PopplerPage, poppler_page, G_TYPE_OBJECT)

PopplerPage *_poppler_page_new(PopplerDocument *document, Page *page, int index)
{
    PopplerPage *poppler_page;

    g_return_val_if_fail(POPPLER_IS_DOCUMENT(document), NULL);

    poppler_page = (PopplerPage *)g_object_new(POPPLER_TYPE_PAGE, nullptr, NULL);
    poppler_page->document = (PopplerDocument *)g_object_ref(document);
    poppler_page->page = page;
    poppler_page->index = index;

    return poppler_page;
}

static void poppler_page_finalize(GObject *object)
{
    PopplerPage *page = POPPLER_PAGE(object);

    g_object_unref(page->document);
    page->document = nullptr;

    if (page->text != nullptr) {
        page->text->decRefCnt();
    }
    /* page->page is owned by the document */

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

/**
 * poppler_page_get_size:
 * @page: A #PopplerPage
 * @width: (out) (allow-none): return location for the width of @page
 * @height: (out) (allow-none): return location for the height of @page
 *
 * Gets the size of @page at the current scale and rotation.
 **/
void poppler_page_get_size(PopplerPage *page, double *width, double *height)
{
    double page_width, page_height;
    int rotate;

    g_return_if_fail(POPPLER_IS_PAGE(page));

    rotate = page->page->getRotate();
    if (rotate == 90 || rotate == 270) {
        page_height = page->page->getCropWidth();
        page_width = page->page->getCropHeight();
    } else {
        page_width = page->page->getCropWidth();
        page_height = page->page->getCropHeight();
    }

    if (width != nullptr) {
        *width = page_width;
    }
    if (height != nullptr) {
        *height = page_height;
    }
}

/**
 * poppler_page_get_index:
 * @page: a #PopplerPage
 *
 * Returns the index of @page
 *
 * Return value: index value of @page
 **/
int poppler_page_get_index(PopplerPage *page)
{
    g_return_val_if_fail(POPPLER_IS_PAGE(page), 0);

    return page->index;
}

/**
 * poppler_page_get_label:
 * @page: a #PopplerPage
 *
 * Returns the label of @page. Note that page labels
 * and page indices might not coincide.
 *
 * Return value: a new allocated string containing the label of @page,
 *               or %NULL if @page doesn't have a label
 *
 * Since: 0.16
 **/
gchar *poppler_page_get_label(PopplerPage *page)
{
    GooString label;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    page->document->doc->getCatalog()->indexToLabel(page->index, &label);
    return _poppler_goo_string_to_utf8(&label);
}

/**
 * poppler_page_get_duration:
 * @page: a #PopplerPage
 *
 * Returns the duration of @page
 *
 * Return value: duration in seconds of @page or -1.
 **/
double poppler_page_get_duration(PopplerPage *page)
{
    g_return_val_if_fail(POPPLER_IS_PAGE(page), -1);

    return page->page->getDuration();
}

/**
 * poppler_page_get_transition:
 * @page: a #PopplerPage
 *
 * Returns the transition effect of @page
 *
 * Return value: a #PopplerPageTransition or %NULL.
 **/
PopplerPageTransition *poppler_page_get_transition(PopplerPage *page)
{
    PageTransition *trans;
    PopplerPageTransition *transition;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    Object obj = page->page->getTrans();
    trans = new PageTransition(&obj);

    if (!trans->isOk()) {
        delete trans;
        return nullptr;
    }

    transition = poppler_page_transition_new();

    switch (trans->getType()) {
    case transitionReplace:
        transition->type = POPPLER_PAGE_TRANSITION_REPLACE;
        break;
    case transitionSplit:
        transition->type = POPPLER_PAGE_TRANSITION_SPLIT;
        break;
    case transitionBlinds:
        transition->type = POPPLER_PAGE_TRANSITION_BLINDS;
        break;
    case transitionBox:
        transition->type = POPPLER_PAGE_TRANSITION_BOX;
        break;
    case transitionWipe:
        transition->type = POPPLER_PAGE_TRANSITION_WIPE;
        break;
    case transitionDissolve:
        transition->type = POPPLER_PAGE_TRANSITION_DISSOLVE;
        break;
    case transitionGlitter:
        transition->type = POPPLER_PAGE_TRANSITION_GLITTER;
        break;
    case transitionFly:
        transition->type = POPPLER_PAGE_TRANSITION_FLY;
        break;
    case transitionPush:
        transition->type = POPPLER_PAGE_TRANSITION_PUSH;
        break;
    case transitionCover:
        transition->type = POPPLER_PAGE_TRANSITION_COVER;
        break;
    case transitionUncover:
        transition->type = POPPLER_PAGE_TRANSITION_UNCOVER;
        break;
    case transitionFade:
        transition->type = POPPLER_PAGE_TRANSITION_FADE;
        break;
    default:
        g_assert_not_reached();
    }

    transition->alignment = (trans->getAlignment() == transitionHorizontal) ? POPPLER_PAGE_TRANSITION_HORIZONTAL : POPPLER_PAGE_TRANSITION_VERTICAL;

    transition->direction = (trans->getDirection() == transitionInward) ? POPPLER_PAGE_TRANSITION_INWARD : POPPLER_PAGE_TRANSITION_OUTWARD;

    transition->duration = trans->getDuration();
    transition->duration_real = trans->getDuration();
    transition->angle = trans->getAngle();
    transition->scale = trans->getScale();
    transition->rectangular = trans->isRectangular();

    delete trans;

    return transition;
}

static TextPage *poppler_page_get_text_page(PopplerPage *page)
{
    if (page->text == nullptr) {
        TextOutputDev *text_dev;
        Gfx *gfx;

        text_dev = new TextOutputDev(nullptr, true, 0, false, false);
        gfx = page->page->createGfx(text_dev, 72.0, 72.0, 0, false, /* useMediaBox */
                                    true, /* Crop */
                                    -1, -1, -1, -1, false, /* printing */
                                    nullptr, nullptr);
        page->page->display(gfx);
        text_dev->endPage();

        page->text = text_dev->takeText();
        delete gfx;
        delete text_dev;
    }

    return page->text;
}

static gboolean annot_is_markup(Annot *annot)
{
    switch (annot->getType()) {
    case Annot::typeLink:
    case Annot::typePopup:
    case Annot::typeMovie:
    case Annot::typeScreen:
    case Annot::typePrinterMark:
    case Annot::typeTrapNet:
    case Annot::typeWatermark:
    case Annot::type3D:
    case Annot::typeWidget:
        return FALSE;
    default:
        return TRUE;
    }
}

static bool poppler_print_annot_cb(Annot *annot, void *user_data)
{
    PopplerPrintFlags user_print_flags = (PopplerPrintFlags)GPOINTER_TO_INT(user_data);

    if (annot->getFlags() & Annot::flagHidden) {
        return false;
    }

    if (user_print_flags & POPPLER_PRINT_STAMP_ANNOTS_ONLY) {
        return (annot->getType() == Annot::typeStamp) ? (annot->getFlags() & Annot::flagPrint) : (annot->getType() == Annot::typeWidget);
    }

    if (user_print_flags & POPPLER_PRINT_MARKUP_ANNOTS) {
        return annot_is_markup(annot) ? (annot->getFlags() & Annot::flagPrint) : (annot->getType() == Annot::typeWidget);
    }

    /* Print document only, form fields are always printed */
    return (annot->getType() == Annot::typeWidget);
}

static void _poppler_page_render(PopplerPage *page, cairo_t *cairo, bool printing, PopplerPrintFlags print_flags)
{
    CairoOutputDev *output_dev;

    g_return_if_fail(POPPLER_IS_PAGE(page));

    output_dev = page->document->output_dev;
    output_dev->setCairo(cairo);
    output_dev->setPrinting(printing);

    if (!printing && page->text == nullptr) {
        page->text = new TextPage(false);
        output_dev->setTextPage(page->text);
    }
    /* NOTE: instead of passing -1 we should/could use cairo_clip_extents()
     * to get a bounding box */
    cairo_save(cairo);
    page->page->displaySlice(output_dev, 72.0, 72.0, 0, false, /* useMediaBox */
                             true, /* Crop */
                             -1, -1, -1, -1, printing, nullptr, nullptr, printing ? poppler_print_annot_cb : nullptr, printing ? GINT_TO_POINTER((gint)print_flags) : nullptr);
    cairo_restore(cairo);

    output_dev->setCairo(nullptr);
    output_dev->setTextPage(nullptr);
}

/**
 * poppler_page_render:
 * @page: the page to render from
 * @cairo: cairo context to render to
 *
 * Render the page to the given cairo context. This function
 * is for rendering a page that will be displayed. If you want
 * to render a page that will be printed use
 * poppler_page_render_for_printing() instead.  Please see the documentation
 * for that function for the differences between rendering to the screen and
 * rendering to a printer.
 **/
void poppler_page_render(PopplerPage *page, cairo_t *cairo)
{
    g_return_if_fail(POPPLER_IS_PAGE(page));

    _poppler_page_render(page, cairo, false, (PopplerPrintFlags)0);
}

/**
 * poppler_page_render_for_printing_with_options:
 * @page: the page to render from
 * @cairo: cairo context to render to
 * @options: print options
 *
 * Render the page to the given cairo context for printing
 * with the specified options
 *
 * See the documentation for poppler_page_render_for_printing() for the
 * differences between rendering to the screen and rendering to a printer.
 *
 * Since: 0.16
 **/
void poppler_page_render_for_printing_with_options(PopplerPage *page, cairo_t *cairo, PopplerPrintFlags options)
{
    g_return_if_fail(POPPLER_IS_PAGE(page));

    _poppler_page_render(page, cairo, true, options);
}

/**
 * poppler_page_render_for_printing:
 * @page: the page to render from
 * @cairo: cairo context to render to
 *
 * Render the page to the given cairo context for printing with
 * #POPPLER_PRINT_ALL flags selected.  If you want a different set of flags,
 * use poppler_page_render_for_printing_with_options().
 *
 * The difference between poppler_page_render() and this function is that some
 * things get rendered differently between screens and printers:
 *
 * <itemizedlist>
 *   <listitem>
 *     PDF annotations get rendered according to their #PopplerAnnotFlag value.
 *     For example, #POPPLER_ANNOT_FLAG_PRINT refers to whether an annotation
 *     is printed or not, whereas #POPPLER_ANNOT_FLAG_NO_VIEW refers to whether
 *     an annotation is invisible when displaying to the screen.
 *   </listitem>
 *   <listitem>
 *     PDF supports "hairlines" of width 0.0, which often get rendered as
 *     having a width of 1 device pixel.  When displaying on a screen, Cairo
 *     may render such lines wide so that they are hard to see, and Poppler
 *     makes use of PDF's Stroke Adjust graphics parameter to make the lines
 *     easier to see.  However, when printing, Poppler is able to directly use a
 *     printer's pixel size instead.
 *   </listitem>
 *   <listitem>
 *     Some advanced features in PDF may require an image to be rasterized
 *     before sending off to a printer.  This may produce raster images which
 *     exceed Cairo's limits.  The "printing" functions will detect this condition
 *     and try to down-scale the intermediate surfaces as appropriate.
 *   </listitem>
 * </itemizedlist>
 *
 **/
void poppler_page_render_for_printing(PopplerPage *page, cairo_t *cairo)
{
    g_return_if_fail(POPPLER_IS_PAGE(page));

    _poppler_page_render(page, cairo, true, POPPLER_PRINT_ALL);
}

static cairo_surface_t *create_surface_from_thumbnail_data(guchar *data, gint width, gint height, gint rowstride)
{
    guchar *cairo_pixels;
    gint cairo_stride;
    cairo_surface_t *surface;
    int j;

    surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
    if (cairo_surface_status(surface)) {
        return nullptr;
    }

    cairo_pixels = cairo_image_surface_get_data(surface);
    cairo_stride = cairo_image_surface_get_stride(surface);

    for (j = height; j; j--) {
        guchar *p = data;
        guchar *q = cairo_pixels;
        guchar *end = p + 3 * width;

        while (p < end) {
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
            q[0] = p[2];
            q[1] = p[1];
            q[2] = p[0];
#else
            q[1] = p[0];
            q[2] = p[1];
            q[3] = p[2];
#endif
            p += 3;
            q += 4;
        }

        data += rowstride;
        cairo_pixels += cairo_stride;
    }

    return surface;
}

/**
 * poppler_page_get_thumbnail:
 * @page: the #PopplerPage to get the thumbnail for
 *
 * Get the embedded thumbnail for the specified page.  If the document
 * doesn't have an embedded thumbnail for the page, this function
 * returns %NULL.
 *
 * Return value: the tumbnail as a cairo_surface_t or %NULL if the document
 * doesn't have a thumbnail for this page.
 **/
cairo_surface_t *poppler_page_get_thumbnail(PopplerPage *page)
{
    unsigned char *data;
    int width, height, rowstride;
    cairo_surface_t *surface;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    if (!page->page->loadThumb(&data, &width, &height, &rowstride)) {
        return nullptr;
    }

    surface = create_surface_from_thumbnail_data(data, width, height, rowstride);
    gfree(data);

    return surface;
}

/**
 * poppler_page_render_selection:
 * @page: the #PopplerPage for which to render selection
 * @cairo: cairo context to render to
 * @selection: start and end point of selection as a rectangle
 * @old_selection: previous selection
 * @style: a #PopplerSelectionStyle
 * @glyph_color: color to use for drawing glyphs
 * @background_color: color to use for the selection background
 *
 * Render the selection specified by @selection for @page to
 * the given cairo context.  The selection will be rendered, using
 * @glyph_color for the glyphs and @background_color for the selection
 * background.
 *
 * If non-NULL, @old_selection specifies the selection that is already
 * rendered to @cairo, in which case this function will (some day)
 * only render the changed part of the selection.
 **/
void poppler_page_render_selection(PopplerPage *page, cairo_t *cairo, PopplerRectangle *selection, PopplerRectangle *old_selection, PopplerSelectionStyle style, PopplerColor *glyph_color, PopplerColor *background_color)
{
    CairoOutputDev *output_dev;
    TextPage *text;
    SelectionStyle selection_style = selectionStyleGlyph;
    PDFRectangle pdf_selection(selection->x1, selection->y1, selection->x2, selection->y2);

    GfxColor gfx_background_color = { { background_color->red, background_color->green, background_color->blue } };
    GfxColor gfx_glyph_color = { { glyph_color->red, glyph_color->green, glyph_color->blue } };

    switch (style) {
    case POPPLER_SELECTION_GLYPH:
        selection_style = selectionStyleGlyph;
        break;
    case POPPLER_SELECTION_WORD:
        selection_style = selectionStyleWord;
        break;
    case POPPLER_SELECTION_LINE:
        selection_style = selectionStyleLine;
        break;
    }

    output_dev = page->document->output_dev;
    output_dev->setCairo(cairo);

    text = poppler_page_get_text_page(page);
    text->drawSelection(output_dev, 1.0, 0, &pdf_selection, selection_style, &gfx_glyph_color, &gfx_background_color);

    output_dev->setCairo(nullptr);
}

/**
 * poppler_page_get_thumbnail_size:
 * @page: A #PopplerPage
 * @width: (out): return location for width
 * @height: (out): return location for height
 *
 * Returns %TRUE if @page has a thumbnail associated with it.  It also
 * fills in @width and @height with the width and height of the
 * thumbnail.  The values of width and height are not changed if no
 * appropriate thumbnail exists.
 *
 * Return value: %TRUE, if @page has a thumbnail associated with it.
 **/
gboolean poppler_page_get_thumbnail_size(PopplerPage *page, int *width, int *height)
{
    Dict *dict;
    gboolean retval = FALSE;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), FALSE);
    g_return_val_if_fail(width != nullptr, FALSE);
    g_return_val_if_fail(height != nullptr, FALSE);

    Object thumb = page->page->getThumb();
    if (!thumb.isStream()) {
        return FALSE;
    }

    dict = thumb.streamGetDict();

    /* Theoretically, this could succeed and you would still fail when
     * loading the thumb */
    if (dict->lookupInt("Width", "W", width) && dict->lookupInt("Height", "H", height)) {
        retval = TRUE;
    }

    return retval;
}

/**
 * poppler_page_get_selection_region:
 * @page: a #PopplerPage
 * @scale: scale specified as pixels per point
 * @style: a #PopplerSelectionStyle
 * @selection: start and end point of selection as a rectangle
 *
 * Returns a region containing the area that would be rendered by
 * poppler_page_render_selection() as a #GList of
 * #PopplerRectangle. The returned list must be freed with
 * poppler_page_selection_region_free().
 *
 * Return value: (element-type PopplerRectangle) (transfer full): a #GList of #PopplerRectangle
 *
 * Deprecated: 0.16: Use poppler_page_get_selected_region() instead.
 **/
GList *poppler_page_get_selection_region(PopplerPage *page, gdouble scale, PopplerSelectionStyle style, PopplerRectangle *selection)
{
    PDFRectangle poppler_selection;
    TextPage *text;
    SelectionStyle selection_style = selectionStyleGlyph;
    GList *region = nullptr;

    poppler_selection.x1 = selection->x1;
    poppler_selection.y1 = selection->y1;
    poppler_selection.x2 = selection->x2;
    poppler_selection.y2 = selection->y2;

    switch (style) {
    case POPPLER_SELECTION_GLYPH:
        selection_style = selectionStyleGlyph;
        break;
    case POPPLER_SELECTION_WORD:
        selection_style = selectionStyleWord;
        break;
    case POPPLER_SELECTION_LINE:
        selection_style = selectionStyleLine;
        break;
    }

    text = poppler_page_get_text_page(page);
    std::vector<PDFRectangle *> *list = text->getSelectionRegion(&poppler_selection, selection_style, scale);

    for (const PDFRectangle *selection_rect : *list) {
        PopplerRectangle *rect;

        rect = poppler_rectangle_new_from_pdf_rectangle(selection_rect);

        region = g_list_prepend(region, rect);

        delete selection_rect;
    }

    delete list;

    return g_list_reverse(region);
}

/**
 * poppler_page_selection_region_free:
 * @region: (element-type PopplerRectangle): a #GList of
 *   #PopplerRectangle
 *
 * Frees @region
 *
 * Deprecated: 0.16: Use only to free deprecated regions created by
 * poppler_page_get_selection_region(). Regions created by
 * poppler_page_get_selected_region() should be freed with
 * cairo_region_destroy() instead.
 */
void poppler_page_selection_region_free(GList *region)
{
    if (G_UNLIKELY(!region)) {
        return;
    }

    g_list_free_full(region, (GDestroyNotify)poppler_rectangle_free);
}

/**
 * poppler_page_get_selected_region:
 * @page: a #PopplerPage
 * @scale: scale specified as pixels per point
 * @style: a #PopplerSelectionStyle
 * @selection: start and end point of selection as a rectangle
 *
 * Returns a region containing the area that would be rendered by
 * poppler_page_render_selection().
 * The returned region must be freed with cairo_region_destroy()
 *
 * Return value: (transfer full): a cairo_region_t
 *
 * Since: 0.16
 **/
cairo_region_t *poppler_page_get_selected_region(PopplerPage *page, gdouble scale, PopplerSelectionStyle style, PopplerRectangle *selection)
{
    PDFRectangle poppler_selection;
    TextPage *text;
    SelectionStyle selection_style = selectionStyleGlyph;
    cairo_region_t *region;

    poppler_selection.x1 = selection->x1;
    poppler_selection.y1 = selection->y1;
    poppler_selection.x2 = selection->x2;
    poppler_selection.y2 = selection->y2;

    switch (style) {
    case POPPLER_SELECTION_GLYPH:
        selection_style = selectionStyleGlyph;
        break;
    case POPPLER_SELECTION_WORD:
        selection_style = selectionStyleWord;
        break;
    case POPPLER_SELECTION_LINE:
        selection_style = selectionStyleLine;
        break;
    }

    text = poppler_page_get_text_page(page);
    std::vector<PDFRectangle *> *list = text->getSelectionRegion(&poppler_selection, selection_style, 1.0);

    region = cairo_region_create();

    for (const PDFRectangle *selection_rect : *list) {
        cairo_rectangle_int_t rect;

        rect.x = (gint)((selection_rect->x1 * scale) + 0.5);
        rect.y = (gint)((selection_rect->y1 * scale) + 0.5);
        rect.width = (gint)(((selection_rect->x2 - selection_rect->x1) * scale) + 0.5);
        rect.height = (gint)(((selection_rect->y2 - selection_rect->y1) * scale) + 0.5);
        cairo_region_union_rectangle(region, &rect);

        delete selection_rect;
    }

    delete list;

    return region;
}

/**
 * poppler_page_get_selected_text:
 * @page: a #PopplerPage
 * @style: a #PopplerSelectionStyle
 * @selection: the #PopplerRectangle including the text
 *
 * Retrieves the contents of the specified @selection as text.
 *
 * Return value: a pointer to the contents of the @selection
 *               as a string
 * Since: 0.16
 **/
char *poppler_page_get_selected_text(PopplerPage *page, PopplerSelectionStyle style, PopplerRectangle *selection)
{
    GooString *sel_text;
    char *result;
    TextPage *text;
    SelectionStyle selection_style = selectionStyleGlyph;
    PDFRectangle pdf_selection;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);
    g_return_val_if_fail(selection != nullptr, NULL);

    pdf_selection.x1 = selection->x1;
    pdf_selection.y1 = selection->y1;
    pdf_selection.x2 = selection->x2;
    pdf_selection.y2 = selection->y2;

    switch (style) {
    case POPPLER_SELECTION_GLYPH:
        selection_style = selectionStyleGlyph;
        break;
    case POPPLER_SELECTION_WORD:
        selection_style = selectionStyleWord;
        break;
    case POPPLER_SELECTION_LINE:
        selection_style = selectionStyleLine;
        break;
    }

    text = poppler_page_get_text_page(page);
    sel_text = text->getSelectionText(&pdf_selection, selection_style);
    result = g_strdup(sel_text->c_str());
    delete sel_text;

    return result;
}

/**
 * poppler_page_get_text:
 * @page: a #PopplerPage
 *
 * Retrieves the text of @page.
 *
 * Return value: a pointer to the text of the @page
 *               as a string
 * Since: 0.16
 **/
char *poppler_page_get_text(PopplerPage *page)
{
    PopplerRectangle rectangle = { 0, 0, 0, 0 };

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    poppler_page_get_size(page, &rectangle.x2, &rectangle.y2);

    return poppler_page_get_selected_text(page, POPPLER_SELECTION_GLYPH, &rectangle);
}

/**
 * poppler_page_get_text_for_area:
 * @page: a #PopplerPage
 * @area: a #PopplerRectangle
 *
 * Retrieves the text of @page contained in @area.
 *
 * Return value: a pointer to the text as a string
 *
 * Since: 0.26
 **/
char *poppler_page_get_text_for_area(PopplerPage *page, PopplerRectangle *area)
{
    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);
    g_return_val_if_fail(area != nullptr, NULL);

    return poppler_page_get_selected_text(page, POPPLER_SELECTION_GLYPH, area);
}

/**
 * poppler_page_find_text_with_options:
 * @page: a #PopplerPage
 * @text: the text to search for (UTF-8 encoded)
 * @options: find options
 *
 * Finds @text in @page with the given #PopplerFindFlags options and
 * returns a #GList of rectangles for each occurrence of the text on the page.
 * The coordinates are in PDF points.
 *
 * When %POPPLER_FIND_MULTILINE is passed in @options, matches may span more than
 * one line. In this case, the returned list will contain one #PopplerRectangle
 * for each part of a match. The function poppler_rectangle_find_get_match_continued()
 * will return %TRUE for all rectangles belonging to the same match, except for
 * the last one. If a hyphen was ignored at the end of the part of the match,
 * poppler_rectangle_find_get_ignored_hyphen() will return %TRUE for that
 * rectangle.
 *
 * Note that currently matches spanning more than two lines are not found.
 * (This limitation may be lifted in a future version.)
 *
 * Note also that currently finding multi-line matches backwards is not
 * implemented; if you pass %POPPLER_FIND_BACKWARDS and %POPPLER_FIND_MULTILINE
 * together, %POPPLER_FIND_MULTILINE will be ignored.
 *
 * Return value: (element-type PopplerRectangle) (transfer full): a newly allocated list
 * of newly allocated #PopplerRectangle. Free with g_list_free_full() using poppler_rectangle_free().
 *
 * Since: 0.22
 **/
GList *poppler_page_find_text_with_options(PopplerPage *page, const char *text, PopplerFindFlags options)
{
    PopplerRectangleExtended *match;
    GList *matches;
    double xMin, yMin, xMax, yMax;
    PDFRectangle continueMatch;
    bool ignoredHyphen;
    gunichar *ucs4;
    glong ucs4_len;
    double height;
    TextPage *text_dev;
    gboolean backwards;
    gboolean start_at_last = FALSE;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);
    g_return_val_if_fail(text != nullptr, NULL);

    text_dev = poppler_page_get_text_page(page);

    ucs4 = g_utf8_to_ucs4_fast(text, -1, &ucs4_len);
    poppler_page_get_size(page, nullptr, &height);

    const bool multiline = (options & POPPLER_FIND_MULTILINE);
    backwards = options & POPPLER_FIND_BACKWARDS;
    matches = nullptr;
    xMin = 0;
    yMin = backwards ? height : 0;

    continueMatch.x1 = std::numeric_limits<double>::max(); // we use this to detect valid returned values

    while (text_dev->findText(ucs4, ucs4_len, false, true, // startAtTop, stopAtBottom
                              start_at_last,
                              false, // stopAtLast
                              options & POPPLER_FIND_CASE_SENSITIVE, options & POPPLER_FIND_IGNORE_DIACRITICS, options & POPPLER_FIND_MULTILINE, backwards, options & POPPLER_FIND_WHOLE_WORDS_ONLY, &xMin, &yMin, &xMax, &yMax, &continueMatch,
                              &ignoredHyphen)) {
        match = poppler_rectangle_extended_new();
        match->x1 = xMin;
        match->y1 = height - yMax;
        match->x2 = xMax;
        match->y2 = height - yMin;
        match->match_continued = false;
        match->ignored_hyphen = false;
        matches = g_list_prepend(matches, match);
        start_at_last = TRUE;

        if (continueMatch.x1 != std::numeric_limits<double>::max()) {
            // received rect for next-line part of a multi-line match, add it.
            if (multiline) {
                match->match_continued = true;
                match->ignored_hyphen = ignoredHyphen;
                match = poppler_rectangle_extended_new();
                match->x1 = continueMatch.x1;
                match->y1 = height - continueMatch.y1;
                match->x2 = continueMatch.x2;
                match->y2 = height - continueMatch.y2;
                match->match_continued = false;
                match->ignored_hyphen = false;
                matches = g_list_prepend(matches, match);
            }

            continueMatch.x1 = std::numeric_limits<double>::max();
        }
    }

    g_free(ucs4);

    return g_list_reverse(matches);
}

/**
 * poppler_page_find_text:
 * @page: a #PopplerPage
 * @text: the text to search for (UTF-8 encoded)
 *
 * Finds @text in @page with the default options (%POPPLER_FIND_DEFAULT) and
 * returns a #GList of rectangles for each occurrence of the text on the page.
 * The coordinates are in PDF points.
 *
 * Return value: (element-type PopplerRectangle) (transfer full): a #GList of #PopplerRectangle,
 **/
GList *poppler_page_find_text(PopplerPage *page, const char *text)
{
    return poppler_page_find_text_with_options(page, text, POPPLER_FIND_DEFAULT);
}

static CairoImageOutputDev *poppler_page_get_image_output_dev(PopplerPage *page, bool (*imgDrawDeviceCbk)(int img_id, void *data), void *imgDrawCbkData)
{
    CairoImageOutputDev *image_dev;
    Gfx *gfx;

    image_dev = new CairoImageOutputDev();

    if (imgDrawDeviceCbk) {
        image_dev->setImageDrawDecideCbk(imgDrawDeviceCbk, imgDrawCbkData);
    }

    gfx = page->page->createGfx(image_dev, 72.0, 72.0, 0, false, /* useMediaBox */
                                true, /* Crop */
                                -1, -1, -1, -1, false, /* printing */
                                nullptr, nullptr);
    page->page->display(gfx);
    delete gfx;

    return image_dev;
}

/**
 * poppler_page_get_image_mapping:
 * @page: A #PopplerPage
 *
 * Returns a list of #PopplerImageMapping items that map from a
 * location on @page to an image of the page. This list must be freed
 * with poppler_page_free_image_mapping() when done.
 *
 * Return value: (element-type PopplerImageMapping) (transfer full): A #GList of #PopplerImageMapping
 **/
GList *poppler_page_get_image_mapping(PopplerPage *page)
{
    GList *map_list = nullptr;
    CairoImageOutputDev *out;
    gint i;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    out = poppler_page_get_image_output_dev(page, nullptr, nullptr);

    for (i = 0; i < out->getNumImages(); i++) {
        PopplerImageMapping *mapping;
        CairoImage *image;

        image = out->getImage(i);

        /* Create the mapping */
        mapping = poppler_image_mapping_new();

        image->getRect(&(mapping->area.x1), &(mapping->area.y1), &(mapping->area.x2), &(mapping->area.y2));
        mapping->image_id = i;

        mapping->area.x1 -= page->page->getCropBox()->x1;
        mapping->area.x2 -= page->page->getCropBox()->x1;
        mapping->area.y1 -= page->page->getCropBox()->y1;
        mapping->area.y2 -= page->page->getCropBox()->y1;

        map_list = g_list_prepend(map_list, mapping);
    }

    delete out;

    return map_list;
}

static bool image_draw_decide_cb(int image_id, void *data)
{
    return (image_id == GPOINTER_TO_INT(data));
}

/**
 * poppler_page_get_image:
 * @page: A #PopplerPage
 * @image_id: The image identifier
 *
 * Returns a cairo surface for the image of the @page
 *
 * Return value: A cairo surface for the image
 **/
cairo_surface_t *poppler_page_get_image(PopplerPage *page, gint image_id)
{
    CairoImageOutputDev *out;
    cairo_surface_t *image;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    out = poppler_page_get_image_output_dev(page, image_draw_decide_cb, GINT_TO_POINTER(image_id));

    if (image_id >= out->getNumImages()) {
        delete out;

        return nullptr;
    }

    image = out->getImage(image_id)->getImage();
    if (!image) {
        delete out;

        return nullptr;
    }

    cairo_surface_reference(image);
    delete out;

    return image;
}

/**
 * poppler_page_free_image_mapping:
 * @list: (element-type PopplerImageMapping): A list of
 *   #PopplerImageMapping<!-- -->s
 *
 * Frees a list of #PopplerImageMapping<!-- -->s allocated by
 * poppler_page_get_image_mapping().
 **/
void poppler_page_free_image_mapping(GList *list)
{
    if (G_UNLIKELY(list == nullptr)) {
        return;
    }

    g_list_free_full(list, (GDestroyNotify)poppler_image_mapping_free);
}

/**
 * poppler_page_render_to_ps:
 * @page: a #PopplerPage
 * @ps_file: the PopplerPSFile to render to
 *
 * Render the page on a postscript file
 *
 **/
void poppler_page_render_to_ps(PopplerPage *page, PopplerPSFile *ps_file)
{
    g_return_if_fail(POPPLER_IS_PAGE(page));
    g_return_if_fail(ps_file != nullptr);

    if (!ps_file->out) {
        std::vector<int> pages;
        for (int i = ps_file->first_page; i <= ps_file->last_page; ++i) {
            pages.push_back(i);
        }
        if (ps_file->fd != -1) {
            ps_file->out =
                    new PSOutputDev(ps_file->fd, ps_file->document->doc, nullptr, pages, psModePS, (int)ps_file->paper_width, (int)ps_file->paper_height, false, ps_file->duplex, 0, 0, 0, 0, psRasterizeWhenNeeded, false, nullptr, nullptr);
        } else {
            ps_file->out = new PSOutputDev(ps_file->filename, ps_file->document->doc, nullptr, pages, psModePS, (int)ps_file->paper_width, (int)ps_file->paper_height, false, ps_file->duplex, 0, 0, 0, 0, psRasterizeWhenNeeded, false,
                                           nullptr, nullptr);
        }
    }

    ps_file->document->doc->displayPage(ps_file->out, page->index + 1, 72.0, 72.0, 0, false, true, false);
}

static void poppler_page_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    PopplerPage *page = POPPLER_PAGE(object);

    switch (prop_id) {
    case PROP_LABEL:
        g_value_take_string(value, poppler_page_get_label(page));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void poppler_page_class_init(PopplerPageClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = poppler_page_finalize;
    gobject_class->get_property = poppler_page_get_property;

    /**
     * PopplerPage:label:
     *
     * The label of the page or %NULL. See also poppler_page_get_label()
     */
    g_object_class_install_property(G_OBJECT_CLASS(klass), PROP_LABEL, g_param_spec_string("label", "Page Label", "The label of the page", nullptr, G_PARAM_READABLE));
}

static void poppler_page_init(PopplerPage *page) { }

/**
 * poppler_page_get_link_mapping:
 * @page: A #PopplerPage
 *
 * Returns a list of #PopplerLinkMapping items that map from a
 * location on @page to a #PopplerAction.  This list must be freed
 * with poppler_page_free_link_mapping() when done.
 *
 * Return value: (element-type PopplerLinkMapping) (transfer full): A #GList of #PopplerLinkMapping
 **/
GList *poppler_page_get_link_mapping(PopplerPage *page)
{
    GList *map_list = nullptr;
    Links *links;
    double width, height;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    links = new Links(page->page->getAnnots());

    if (links == nullptr) {
        return nullptr;
    }

    poppler_page_get_size(page, &width, &height);

    for (AnnotLink *link : links->getLinks()) {
        PopplerLinkMapping *mapping;
        PopplerRectangle rect;
        LinkAction *link_action;

        link_action = link->getAction();

        /* Create the mapping */
        mapping = poppler_link_mapping_new();
        mapping->action = _poppler_action_new(page->document, link_action, nullptr);

        link->getRect(&rect.x1, &rect.y1, &rect.x2, &rect.y2);

        rect.x1 -= page->page->getCropBox()->x1;
        rect.x2 -= page->page->getCropBox()->x1;
        rect.y1 -= page->page->getCropBox()->y1;
        rect.y2 -= page->page->getCropBox()->y1;

        switch (page->page->getRotate()) {
        case 90:
            mapping->area.x1 = rect.y1;
            mapping->area.y1 = height - rect.x2;
            mapping->area.x2 = mapping->area.x1 + (rect.y2 - rect.y1);
            mapping->area.y2 = mapping->area.y1 + (rect.x2 - rect.x1);

            break;
        case 180:
            mapping->area.x1 = width - rect.x2;
            mapping->area.y1 = height - rect.y2;
            mapping->area.x2 = mapping->area.x1 + (rect.x2 - rect.x1);
            mapping->area.y2 = mapping->area.y1 + (rect.y2 - rect.y1);

            break;
        case 270:
            mapping->area.x1 = width - rect.y2;
            mapping->area.y1 = rect.x1;
            mapping->area.x2 = mapping->area.x1 + (rect.y2 - rect.y1);
            mapping->area.y2 = mapping->area.y1 + (rect.x2 - rect.x1);

            break;
        default:
            mapping->area.x1 = rect.x1;
            mapping->area.y1 = rect.y1;
            mapping->area.x2 = rect.x2;
            mapping->area.y2 = rect.y2;
        }

        map_list = g_list_prepend(map_list, mapping);
    }

    delete links;

    return map_list;
}

/**
 * poppler_page_free_link_mapping:
 * @list: (element-type PopplerLinkMapping): A list of
 *   #PopplerLinkMapping<!-- -->s
 *
 * Frees a list of #PopplerLinkMapping<!-- -->s allocated by
 * poppler_page_get_link_mapping().  It also frees the #PopplerAction<!-- -->s
 * that each mapping contains, so if you want to keep them around, you need to
 * copy them with poppler_action_copy().
 **/
void poppler_page_free_link_mapping(GList *list)
{
    if (G_UNLIKELY(list == nullptr)) {
        return;
    }

    g_list_free_full(list, (GDestroyNotify)poppler_link_mapping_free);
}

/**
 * poppler_page_get_form_field_mapping:
 * @page: A #PopplerPage
 *
 * Returns a list of #PopplerFormFieldMapping items that map from a
 * location on @page to a form field.  This list must be freed
 * with poppler_page_free_form_field_mapping() when done.
 *
 * Return value: (element-type PopplerFormFieldMapping) (transfer full): A #GList of #PopplerFormFieldMapping
 **/
GList *poppler_page_get_form_field_mapping(PopplerPage *page)
{
    GList *map_list = nullptr;
    gint i;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    const std::unique_ptr<FormPageWidgets> forms = page->page->getFormWidgets();

    if (forms == nullptr) {
        return nullptr;
    }

    for (i = 0; i < forms->getNumWidgets(); i++) {
        PopplerFormFieldMapping *mapping;
        FormWidget *field;

        mapping = poppler_form_field_mapping_new();

        field = forms->getWidget(i);

        mapping->field = _poppler_form_field_new(page->document, field);
        field->getRect(&(mapping->area.x1), &(mapping->area.y1), &(mapping->area.x2), &(mapping->area.y2));

        mapping->area.x1 -= page->page->getCropBox()->x1;
        mapping->area.x2 -= page->page->getCropBox()->x1;
        mapping->area.y1 -= page->page->getCropBox()->y1;
        mapping->area.y2 -= page->page->getCropBox()->y1;

        map_list = g_list_prepend(map_list, mapping);
    }

    return map_list;
}

/**
 * poppler_page_free_form_field_mapping:
 * @list: (element-type PopplerFormFieldMapping): A list of
 *   #PopplerFormFieldMapping<!-- -->s
 *
 * Frees a list of #PopplerFormFieldMapping<!-- -->s allocated by
 * poppler_page_get_form_field_mapping().
 **/
void poppler_page_free_form_field_mapping(GList *list)
{
    if (G_UNLIKELY(list == nullptr)) {
        return;
    }

    g_list_free_full(list, (GDestroyNotify)poppler_form_field_mapping_free);
}

/**
 * poppler_page_get_annot_mapping:
 * @page: A #PopplerPage
 *
 * Returns a list of #PopplerAnnotMapping items that map from a location on
 * @page to a #PopplerAnnot.  This list must be freed with
 * poppler_page_free_annot_mapping() when done.
 *
 * Return value: (element-type PopplerAnnotMapping) (transfer full): A #GList of #PopplerAnnotMapping
 **/
GList *poppler_page_get_annot_mapping(PopplerPage *page)
{
    GList *map_list = nullptr;
    double width, height;
    Annots *annots;
    const PDFRectangle *crop_box;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    annots = page->page->getAnnots();
    if (!annots) {
        return nullptr;
    }

    poppler_page_get_size(page, &width, &height);
    crop_box = page->page->getCropBox();

    for (Annot *annot : annots->getAnnots()) {
        PopplerAnnotMapping *mapping;
        PopplerRectangle rect;
        gboolean flag_no_rotate;
        gint rotation = 0;

        flag_no_rotate = annot->getFlags() & Annot::flagNoRotate;

        /* Create the mapping */
        mapping = poppler_annot_mapping_new();

        switch (annot->getType()) {
        case Annot::typeText:
            mapping->annot = _poppler_annot_text_new(annot);
            break;
        case Annot::typeFreeText:
            mapping->annot = _poppler_annot_free_text_new(annot);
            break;
        case Annot::typeFileAttachment:
            mapping->annot = _poppler_annot_file_attachment_new(annot);
            break;
        case Annot::typeMovie:
            mapping->annot = _poppler_annot_movie_new(annot);
            break;
        case Annot::typeScreen:
            mapping->annot = _poppler_annot_screen_new(page->document, annot);
            break;
        case Annot::typeLine:
            mapping->annot = _poppler_annot_line_new(annot);
            break;
        case Annot::typeSquare:
            mapping->annot = _poppler_annot_square_new(annot);
            break;
        case Annot::typeCircle:
            mapping->annot = _poppler_annot_circle_new(annot);
            break;
        case Annot::typeHighlight:
        case Annot::typeUnderline:
        case Annot::typeSquiggly:
        case Annot::typeStrikeOut:
            mapping->annot = _poppler_annot_text_markup_new(annot);
            break;
        default:
            mapping->annot = _poppler_annot_new(annot);
            break;
        }

        const PDFRectangle &annot_rect = annot->getRect();
        rect.x1 = annot_rect.x1 - crop_box->x1;
        rect.y1 = annot_rect.y1 - crop_box->y1;
        rect.x2 = annot_rect.x2 - crop_box->x1;
        rect.y2 = annot_rect.y2 - crop_box->y1;

        rotation = page->page->getRotate();

        if (rotation == 0 || !SUPPORTED_ROTATION(rotation)) { /* zero or unknown rotation */
            mapping->area.x1 = rect.x1;
            mapping->area.y1 = rect.y1;
            mapping->area.x2 = rect.x2;
            mapping->area.y2 = rect.y2;
        } else {
            gdouble annot_height = rect.y2 - rect.y1;
            gdouble annot_width = rect.x2 - rect.x1;

            if (flag_no_rotate) {
                if (rotation == 90) {
                    mapping->area.x1 = rect.y2;
                    mapping->area.y1 = height - (rect.x1 + annot_height);
                    mapping->area.x2 = rect.y2 + annot_width;
                    mapping->area.y2 = height - rect.x1;
                } else if (rotation == 180) {
                    mapping->area.x1 = width - rect.x1;
                    mapping->area.x2 = MIN(mapping->area.x1 + annot_width, width);
                    mapping->area.y2 = height - rect.y2;
                    mapping->area.y1 = MAX(0, mapping->area.y2 - annot_height);
                } else if (rotation == 270) {
                    mapping->area.x1 = width - rect.y2;
                    mapping->area.x2 = MIN(mapping->area.x1 + annot_width, width);
                    mapping->area.y2 = rect.x1;
                    mapping->area.y1 = MAX(0, mapping->area.y2 - annot_height);
                }
            } else { /* !flag_no_rotate */
                if (rotation == 90) {
                    mapping->area.x1 = rect.y1;
                    mapping->area.y1 = height - rect.x2;
                    mapping->area.x2 = mapping->area.x1 + annot_height;
                    mapping->area.y2 = mapping->area.y1 + annot_width;
                } else if (rotation == 180) {
                    mapping->area.x1 = width - rect.x2;
                    mapping->area.y1 = height - rect.y2;
                    mapping->area.x2 = mapping->area.x1 + annot_width;
                    mapping->area.y2 = mapping->area.y1 + annot_height;
                } else if (rotation == 270) {
                    mapping->area.x1 = width - rect.y2;
                    mapping->area.y1 = rect.x1;
                    mapping->area.x2 = mapping->area.x1 + annot_height;
                    mapping->area.y2 = mapping->area.y1 + annot_width;
                }
            }
        }

        map_list = g_list_prepend(map_list, mapping);
    }

    return g_list_reverse(map_list);
}

/**
 * poppler_page_free_annot_mapping:
 * @list: (element-type PopplerAnnotMapping): A list of
 *   #PopplerAnnotMapping<!-- -->s
 *
 * Frees a list of #PopplerAnnotMapping<!-- -->s allocated by
 * poppler_page_get_annot_mapping().  It also unreferences the #PopplerAnnot<!-- -->s
 * that each mapping contains, so if you want to keep them around, you need to
 * reference them with g_object_ref().
 **/
void poppler_page_free_annot_mapping(GList *list)
{
    if (G_UNLIKELY(!list)) {
        return;
    }

    g_list_free_full(list, (GDestroyNotify)poppler_annot_mapping_free);
}

/* Adds or removes (according to @add parameter) the passed in @crop_box from the
 * passed in @quads and returns it as a new #AnnotQuadrilaterals object */
AnnotQuadrilaterals *new_quads_from_offset_cropbox(const PDFRectangle *crop_box, AnnotQuadrilaterals *quads, gboolean add)
{
    int len = quads->getQuadrilateralsLength();
    auto quads_array = std::make_unique<AnnotQuadrilaterals::AnnotQuadrilateral[]>(len);
    for (int i = 0; i < len; i++) {
        if (add) {
            quads_array[i] = AnnotQuadrilaterals::AnnotQuadrilateral(quads->getX1(i) + crop_box->x1, quads->getY1(i) + crop_box->y1, quads->getX2(i) + crop_box->x1, quads->getY2(i) + crop_box->y1, quads->getX3(i) + crop_box->x1,
                                                                     quads->getY3(i) + crop_box->y1, quads->getX4(i) + crop_box->x1, quads->getY4(i) + crop_box->y1);
        } else {
            quads_array[i] = AnnotQuadrilaterals::AnnotQuadrilateral(quads->getX1(i) - crop_box->x1, quads->getY1(i) - crop_box->y1, quads->getX2(i) - crop_box->x1, quads->getY2(i) - crop_box->y1, quads->getX3(i) - crop_box->x1,
                                                                     quads->getY3(i) - crop_box->y1, quads->getX4(i) - crop_box->x1, quads->getY4(i) - crop_box->y1);
        }
    }

    return new AnnotQuadrilaterals(std::move(quads_array), len);
}

/* This function undoes the rotation of @page in the passed-in @x @y point.
 * In other words, it moves the point to where it'll be located if @page
 * was put to zero rotation (unrotated) */
static void _page_unrotate_xy(Page *page, double *x, double *y)
{
    double page_width, page_height, temp;
    gint rotation = page->getRotate();

    if (rotation == 90 || rotation == 270) {
        page_height = page->getCropWidth();
        page_width = page->getCropHeight();
    } else {
        page_width = page->getCropWidth();
        page_height = page->getCropHeight();
    }

    if (rotation == 90) {
        temp = *x;
        *x = page_height - *y;
        *y = temp;
    } else if (rotation == 180) {
        *x = page_width - *x;
        *y = page_height - *y;
    } else if (rotation == 270) {
        temp = *x;
        *x = *y;
        *y = page_width - temp;
    }
}

AnnotQuadrilaterals *_page_new_quads_unrotated(Page *page, AnnotQuadrilaterals *quads)
{
    double x1, y1, x2, y2, x3, y3, x4, y4;
    int len = quads->getQuadrilateralsLength();
    auto quads_array = std::make_unique<AnnotQuadrilaterals::AnnotQuadrilateral[]>(len);

    for (int i = 0; i < len; i++) {
        x1 = quads->getX1(i);
        y1 = quads->getY1(i);
        x2 = quads->getX2(i);
        y2 = quads->getY2(i);
        x3 = quads->getX3(i);
        y3 = quads->getY3(i);
        x4 = quads->getX4(i);
        y4 = quads->getY4(i);

        _page_unrotate_xy(page, &x1, &y1);
        _page_unrotate_xy(page, &x2, &y2);
        _page_unrotate_xy(page, &x3, &y3);
        _page_unrotate_xy(page, &x4, &y4);

        quads_array[i] = AnnotQuadrilaterals::AnnotQuadrilateral(x1, y1, x2, y2, x3, y3, x4, y4);
    }

    return new AnnotQuadrilaterals(std::move(quads_array), len);
}

/* @x1 @y1 @x2 @y2 are both 'in' and 'out' parameters, representing
 * the diagonal of a rectangle which is the 'rect' area of @annot
 * which is inside @page.
 *
 * If @page is unrotated (i.e. has zero rotation) this function does
 * nothing, otherwise this function un-rotates the passed-in rect
 * coords according to @page rotation so as the returned coords are
 * those of the rect if page was put to zero rotation (unrotated).
 * This is mandated by PDF spec when saving annotation coords (see
 * 8.4.2 Annotation Flags) which also explains the special rotation
 * that needs to be done when @annot has the flagNoRotate set to
 * true, which this function follows. */
void _unrotate_rect_for_annot_and_page(Page *page, Annot *annot, double *x1, double *y1, double *x2, double *y2)
{
    gboolean flag_no_rotate;

    if (!SUPPORTED_ROTATION(page->getRotate())) {
        return;
    }
    /* Normalize received rect diagonal to be from UpperLeft to BottomRight,
     * as our algorithm follows that */
    if (*y2 > *y1) {
        double temp = *y1;
        *y1 = *y2;
        *y2 = temp;
    }
    if (G_UNLIKELY(*x1 > *x2)) {
        double temp = *x1;
        *x1 = *x2;
        *x2 = temp;
    }
    flag_no_rotate = annot->getFlags() & Annot::flagNoRotate;
    if (flag_no_rotate) {
        /* For this case rotating just the upperleft point is enough */
        double annot_height = *y1 - *y2;
        double annot_width = *x2 - *x1;
        _page_unrotate_xy(page, x1, y1);
        *x2 = *x1 + annot_width;
        *y2 = *y1 - annot_height;
    } else {
        _page_unrotate_xy(page, x1, y1);
        _page_unrotate_xy(page, x2, y2);
    }
}

/**
 * poppler_page_add_annot:
 * @page: a #PopplerPage
 * @annot: a #PopplerAnnot to add
 *
 * Adds annotation @annot to @page.
 *
 * Since: 0.16
 */
void poppler_page_add_annot(PopplerPage *page, PopplerAnnot *annot)
{
    double x1, y1, x2, y2;
    gboolean page_is_rotated;
    const PDFRectangle *crop_box;
    const PDFRectangle *page_crop_box;

    g_return_if_fail(POPPLER_IS_PAGE(page));
    g_return_if_fail(POPPLER_IS_ANNOT(annot));

    /* Add the page's cropBox to the coordinates of rect field of annot */
    page_crop_box = page->page->getCropBox();
    annot->annot->getRect(&x1, &y1, &x2, &y2);

    page_is_rotated = SUPPORTED_ROTATION(page->page->getRotate());
    if (page_is_rotated) {
        /* annot is inside a rotated page, as core poppler rect must be saved
         * un-rotated, let's proceed to un-rotate rect before saving */
        _unrotate_rect_for_annot_and_page(page->page, annot->annot, &x1, &y1, &x2, &y2);
    }

    annot->annot->setRect(x1 + page_crop_box->x1, y1 + page_crop_box->y1, x2 + page_crop_box->x1, y2 + page_crop_box->y1);

    AnnotTextMarkup *annot_markup = dynamic_cast<AnnotTextMarkup *>(annot->annot);
    if (annot_markup) {
        AnnotQuadrilaterals *quads;
        crop_box = _poppler_annot_get_cropbox(annot);
        if (crop_box) {
            /* Handle hypothetical case of annot being added is already existing on a prior page, so
             * first remove cropbox of the prior page before adding cropbox of the new page later */
            quads = new_quads_from_offset_cropbox(crop_box, annot_markup->getQuadrilaterals(), FALSE);
            annot_markup->setQuadrilaterals(quads);
        }
        if (page_is_rotated) {
            /* Quadrilateral's coords need to be saved un-rotated (same as rect coords) */
            quads = _page_new_quads_unrotated(page->page, annot_markup->getQuadrilaterals());
            annot_markup->setQuadrilaterals(quads);
        }
        /* Add to annot's quadrilaterals the offset for the cropbox of the new page */
        quads = new_quads_from_offset_cropbox(page_crop_box, annot_markup->getQuadrilaterals(), TRUE);
        annot_markup->setQuadrilaterals(quads);
    }

    page->page->addAnnot(annot->annot);
}

/**
 * poppler_page_remove_annot:
 * @page: a #PopplerPage
 * @annot: a #PopplerAnnot to remove
 *
 * Removes annotation @annot from @page
 *
 * Since: 0.22
 */
void poppler_page_remove_annot(PopplerPage *page, PopplerAnnot *annot)
{
    g_return_if_fail(POPPLER_IS_PAGE(page));
    g_return_if_fail(POPPLER_IS_ANNOT(annot));

    page->page->removeAnnot(annot->annot);
}

/* PopplerRectangle type */

G_DEFINE_BOXED_TYPE(PopplerRectangle, poppler_rectangle, poppler_rectangle_copy, poppler_rectangle_free)

static PopplerRectangleExtended *poppler_rectangle_extended_new()
{
    return g_slice_new0(PopplerRectangleExtended);
}

PopplerRectangle *poppler_rectangle_new_from_pdf_rectangle(const PDFRectangle *rect)
{
    auto r = poppler_rectangle_extended_new();
    r->x1 = rect->x1;
    r->y1 = rect->y1;
    r->x2 = rect->x2;
    r->y2 = rect->y2;

    return reinterpret_cast<PopplerRectangle *>(r);
}

/**
 * poppler_rectangle_new:
 *
 * Creates a new #PopplerRectangle
 *
 * Returns: a new #PopplerRectangle, use poppler_rectangle_free() to free it
 */
PopplerRectangle *poppler_rectangle_new(void)
{
    return reinterpret_cast<PopplerRectangle *>(poppler_rectangle_extended_new());
}

/**
 * poppler_rectangle_copy:
 * @rectangle: a #PopplerRectangle to copy
 *
 * Creates a copy of @rectangle.
 *
 * Note that you must only use this function on an allocated PopplerRectangle, as
 * returned by poppler_rectangle_new(), poppler_rectangle_copy(), or the list elements
 * returned from poppler_page_find_text() or poppler_page_find_text_with_options().
 * Returns: a new allocated copy of @rectangle
 */
PopplerRectangle *poppler_rectangle_copy(PopplerRectangle *rectangle)
{
    g_return_val_if_fail(rectangle != nullptr, NULL);

    auto ext_rectangle = reinterpret_cast<PopplerRectangleExtended *>(rectangle);
    return reinterpret_cast<PopplerRectangle *>(g_slice_dup(PopplerRectangleExtended, ext_rectangle));
}

/**
 * poppler_rectangle_free:
 * @rectangle: a #PopplerRectangle
 *
 * Frees the given #PopplerRectangle.
 *
 * Note that you must only use this function on an allocated PopplerRectangle, as
 * returned by poppler_rectangle_new(), poppler_rectangle_copy(), or the list elements
 * returned from poppler_page_find_text() or poppler_page_find_text_with_options().
 */
void poppler_rectangle_free(PopplerRectangle *rectangle)
{
    auto ext_rectangle = reinterpret_cast<PopplerRectangleExtended *>(rectangle);
    g_slice_free(PopplerRectangleExtended, ext_rectangle);
}

/**
 * poppler_rectangle_find_get_match_continued:
 * @rectangle: a #PopplerRectangle
 *
 * When using poppler_page_find_text_with_options() with the
 * %POPPLER_FIND_MULTILINE flag, a match may span more than one line
 * and thus consist of more than one rectangle. Every rectangle belonging
 * to the same match will return %TRUE from this function, except for
 * the last rectangle, where this function will return %FALSE.
 *
 * Note that you must only call this function on a #PopplerRectangle
 * returned in the list from poppler_page_find_text() or
 * poppler_page_find_text_with_options().
 *
 * Returns: whether there are more rectangles belonging to the same match
 *
 * Since: 21.05.0
 */
gboolean poppler_rectangle_find_get_match_continued(const PopplerRectangle *rectangle)
{
    g_return_val_if_fail(rectangle != nullptr, false);

    auto ext_rectangle = reinterpret_cast<const PopplerRectangleExtended *>(rectangle);
    return ext_rectangle->match_continued;
}

/**
 * poppler_rectangle_find_get_ignored_hyphen:
 * @rectangle: a #PopplerRectangle
 *
 * When using poppler_page_find_text_with_options() with the
 * %POPPLER_FIND_MULTILINE flag, a match may span more than one line,
 * and may have been formed by ignoring a hyphen at the end of the line.
 * When this happens at the end of the line corresponding to @rectangle,
 * this function returns %TRUE (and then poppler_rectangle_find_get_match_continued()
 * will also return %TRUE); otherwise it returns %FALSE.
 *
 * Note that you must only call this function on a #PopplerRectangle
 * returned in the list from poppler_page_find_text() or
 * poppler_page_find_text_with_options().
 *
 * Returns: whether a hyphen was ignored at the end of the line corresponding to @rectangle.
 *
 * Since: 21.05.0
 */
gboolean poppler_rectangle_find_get_ignored_hyphen(const PopplerRectangle *rectangle)
{
    g_return_val_if_fail(rectangle != nullptr, false);

    auto ext_rectangle = reinterpret_cast<const PopplerRectangleExtended *>(rectangle);
    return ext_rectangle->ignored_hyphen;
}

G_DEFINE_BOXED_TYPE(PopplerPoint, poppler_point, poppler_point_copy, poppler_point_free)

/**
 * poppler_point_new:
 *
 * Creates a new #PopplerPoint. It must be freed with poppler_point_free() after use.
 *
 * Returns: a new #PopplerPoint
 *
 * Since: 0.26
 **/
PopplerPoint *poppler_point_new(void)
{
    return g_slice_new0(PopplerPoint);
}

/**
 * poppler_point_copy:
 * @point: a #PopplerPoint to copy
 *
 * Creates a copy of @point. The copy must be freed with poppler_point_free()
 * after use.
 *
 * Returns: a new allocated copy of @point
 *
 * Since: 0.26
 **/
PopplerPoint *poppler_point_copy(PopplerPoint *point)
{
    g_return_val_if_fail(point != nullptr, NULL);

    return g_slice_dup(PopplerPoint, point);
}

/**
 * poppler_point_free:
 * @point: a #PopplerPoint
 *
 * Frees the memory used by @point
 *
 * Since: 0.26
 **/
void poppler_point_free(PopplerPoint *point)
{
    g_slice_free(PopplerPoint, point);
}

/* PopplerQuadrilateral type */

G_DEFINE_BOXED_TYPE(PopplerQuadrilateral, poppler_quadrilateral, poppler_quadrilateral_copy, poppler_quadrilateral_free)

/**
 * poppler_quadrilateral_new:
 *
 * Creates a new #PopplerQuadrilateral. It must be freed with poppler_quadrilateral_free() after use.
 *
 * Returns: a new #PopplerQuadrilateral.
 *
 * Since: 0.26
 **/
PopplerQuadrilateral *poppler_quadrilateral_new(void)
{
    return g_slice_new0(PopplerQuadrilateral);
}

/**
 * poppler_quadrilateral_copy:
 * @quad: a #PopplerQuadrilateral to copy
 *
 * Creates a copy of @quad. The copy must be freed with poppler_quadrilateral_free() after use.
 *
 * Returns: a new allocated copy of @quad
 *
 * Since: 0.26
 **/
PopplerQuadrilateral *poppler_quadrilateral_copy(PopplerQuadrilateral *quad)
{
    g_return_val_if_fail(quad != nullptr, NULL);

    return g_slice_dup(PopplerQuadrilateral, quad);
}

/**
 * poppler_quadrilateral_free:
 * @quad: a #PopplerQuadrilateral
 *
 * Frees the memory used by @quad
 *
 * Since: 0.26
 **/
void poppler_quadrilateral_free(PopplerQuadrilateral *quad)
{
    g_slice_free(PopplerQuadrilateral, quad);
}

/* PopplerTextAttributes type */

G_DEFINE_BOXED_TYPE(PopplerTextAttributes, poppler_text_attributes, poppler_text_attributes_copy, poppler_text_attributes_free)

/**
 * poppler_text_attributes_new:
 *
 * Creates a new #PopplerTextAttributes
 *
 * Returns: a new #PopplerTextAttributes, use poppler_text_attributes_free() to free it
 *
 * Since: 0.18
 */
PopplerTextAttributes *poppler_text_attributes_new(void)
{
    return (PopplerTextAttributes *)g_slice_new0(PopplerTextAttributes);
}

static gchar *get_font_name_from_word(const TextWord *word, gint word_i)
{
    const GooString *font_name = word->getFontName(word_i);
    const gchar *name;
    gboolean subset;
    gint i;

    if (!font_name || font_name->getLength() == 0) {
        return g_strdup("Default");
    }

    // check for a font subset name: capital letters followed by a '+' sign
    for (i = 0; i < font_name->getLength(); ++i) {
        if (font_name->getChar(i) < 'A' || font_name->getChar(i) > 'Z') {
            break;
        }
    }
    subset = i > 0 && i < font_name->getLength() && font_name->getChar(i) == '+';
    name = font_name->c_str();
    if (subset) {
        name += i + 1;
    }

    return g_strdup(name);
}

/*
 * Allocates a new PopplerTextAttributes with word attributes
 */
static PopplerTextAttributes *poppler_text_attributes_new_from_word(const TextWord *word, gint i)
{
    PopplerTextAttributes *attrs = poppler_text_attributes_new();
    gdouble r, g, b;

    attrs->font_name = get_font_name_from_word(word, i);
    attrs->font_size = word->getFontSize();
    attrs->is_underlined = word->isUnderlined();
    word->getColor(&r, &g, &b);
    attrs->color.red = (int)(r * 65535. + 0.5);
    attrs->color.green = (int)(g * 65535. + 0.5);
    attrs->color.blue = (int)(b * 65535. + 0.5);

    return attrs;
}

/**
 * poppler_text_attributes_copy:
 * @text_attrs: a #PopplerTextAttributes to copy
 *
 * Creates a copy of @text_attrs
 *
 * Returns: a new allocated copy of @text_attrs
 *
 * Since: 0.18
 */
PopplerTextAttributes *poppler_text_attributes_copy(PopplerTextAttributes *text_attrs)
{
    PopplerTextAttributes *attrs;

    attrs = g_slice_dup(PopplerTextAttributes, text_attrs);
    attrs->font_name = g_strdup(text_attrs->font_name);
    return attrs;
}

/**
 * poppler_text_attributes_free:
 * @text_attrs: a #PopplerTextAttributes
 *
 * Frees the given #PopplerTextAttributes
 *
 * Since: 0.18
 */
void poppler_text_attributes_free(PopplerTextAttributes *text_attrs)
{
    g_free(text_attrs->font_name);
    g_slice_free(PopplerTextAttributes, text_attrs);
}

/**
 * SECTION:poppler-color
 * @short_description: Colors
 * @title: PopplerColor
 */

/* PopplerColor type */
G_DEFINE_BOXED_TYPE(PopplerColor, poppler_color, poppler_color_copy, poppler_color_free)

/**
 * poppler_color_new:
 *
 * Creates a new #PopplerColor
 *
 * Returns: a new #PopplerColor, use poppler_color_free() to free it
 */
PopplerColor *poppler_color_new(void)
{
    return (PopplerColor *)g_new0(PopplerColor, 1);
}

/**
 * poppler_color_copy:
 * @color: a #PopplerColor to copy
 *
 * Creates a copy of @color
 *
 * Returns: a new allocated copy of @color
 */
PopplerColor *poppler_color_copy(PopplerColor *color)
{
    PopplerColor *new_color;

    new_color = g_new(PopplerColor, 1);
    *new_color = *color;

    return new_color;
}

/**
 * poppler_color_free:
 * @color: a #PopplerColor
 *
 * Frees the given #PopplerColor
 */
void poppler_color_free(PopplerColor *color)
{
    g_free(color);
}

/* PopplerLinkMapping type */
G_DEFINE_BOXED_TYPE(PopplerLinkMapping, poppler_link_mapping, poppler_link_mapping_copy, poppler_link_mapping_free)

/**
 * poppler_link_mapping_new:
 *
 * Creates a new #PopplerLinkMapping
 *
 * Returns: a new #PopplerLinkMapping, use poppler_link_mapping_free() to free it
 */
PopplerLinkMapping *poppler_link_mapping_new(void)
{
    return g_slice_new0(PopplerLinkMapping);
}

/**
 * poppler_link_mapping_copy:
 * @mapping: a #PopplerLinkMapping to copy
 *
 * Creates a copy of @mapping
 *
 * Returns: a new allocated copy of @mapping
 */
PopplerLinkMapping *poppler_link_mapping_copy(PopplerLinkMapping *mapping)
{
    PopplerLinkMapping *new_mapping;

    new_mapping = g_slice_dup(PopplerLinkMapping, mapping);

    if (new_mapping->action) {
        new_mapping->action = poppler_action_copy(new_mapping->action);
    }

    return new_mapping;
}

/**
 * poppler_link_mapping_free:
 * @mapping: a #PopplerLinkMapping
 *
 * Frees the given #PopplerLinkMapping
 */
void poppler_link_mapping_free(PopplerLinkMapping *mapping)
{
    if (G_UNLIKELY(!mapping)) {
        return;
    }

    if (mapping->action) {
        poppler_action_free(mapping->action);
    }

    g_slice_free(PopplerLinkMapping, mapping);
}

/* Poppler Image mapping type */
G_DEFINE_BOXED_TYPE(PopplerImageMapping, poppler_image_mapping, poppler_image_mapping_copy, poppler_image_mapping_free)

/**
 * poppler_image_mapping_new:
 *
 * Creates a new #PopplerImageMapping
 *
 * Returns: a new #PopplerImageMapping, use poppler_image_mapping_free() to free it
 */
PopplerImageMapping *poppler_image_mapping_new(void)
{
    return g_slice_new0(PopplerImageMapping);
}

/**
 * poppler_image_mapping_copy:
 * @mapping: a #PopplerImageMapping to copy
 *
 * Creates a copy of @mapping
 *
 * Returns: a new allocated copy of @mapping
 */
PopplerImageMapping *poppler_image_mapping_copy(PopplerImageMapping *mapping)
{
    return g_slice_dup(PopplerImageMapping, mapping);
}

/**
 * poppler_image_mapping_free:
 * @mapping: a #PopplerImageMapping
 *
 * Frees the given #PopplerImageMapping
 */
void poppler_image_mapping_free(PopplerImageMapping *mapping)
{
    g_slice_free(PopplerImageMapping, mapping);
}

/* Page Transition */
G_DEFINE_BOXED_TYPE(PopplerPageTransition, poppler_page_transition, poppler_page_transition_copy, poppler_page_transition_free)

/**
 * poppler_page_transition_new:
 *
 * Creates a new #PopplerPageTransition
 *
 * Returns: a new #PopplerPageTransition, use poppler_page_transition_free() to free it
 */
PopplerPageTransition *poppler_page_transition_new(void)
{
    return (PopplerPageTransition *)g_new0(PopplerPageTransition, 1);
}

/**
 * poppler_page_transition_copy:
 * @transition: a #PopplerPageTransition to copy
 *
 * Creates a copy of @transition
 *
 * Returns: a new allocated copy of @transition
 */
PopplerPageTransition *poppler_page_transition_copy(PopplerPageTransition *transition)
{
    PopplerPageTransition *new_transition;

    new_transition = poppler_page_transition_new();
    *new_transition = *transition;

    return new_transition;
}

/**
 * poppler_page_transition_free:
 * @transition: a #PopplerPageTransition
 *
 * Frees the given #PopplerPageTransition
 */
void poppler_page_transition_free(PopplerPageTransition *transition)
{
    g_free(transition);
}

/* Form Field Mapping Type */
G_DEFINE_BOXED_TYPE(PopplerFormFieldMapping, poppler_form_field_mapping, poppler_form_field_mapping_copy, poppler_form_field_mapping_free)

/**
 * poppler_form_field_mapping_new:
 *
 * Creates a new #PopplerFormFieldMapping
 *
 * Returns: a new #PopplerFormFieldMapping, use poppler_form_field_mapping_free() to free it
 */
PopplerFormFieldMapping *poppler_form_field_mapping_new(void)
{
    return g_slice_new0(PopplerFormFieldMapping);
}

/**
 * poppler_form_field_mapping_copy:
 * @mapping: a #PopplerFormFieldMapping to copy
 *
 * Creates a copy of @mapping
 *
 * Returns: a new allocated copy of @mapping
 */
PopplerFormFieldMapping *poppler_form_field_mapping_copy(PopplerFormFieldMapping *mapping)
{
    PopplerFormFieldMapping *new_mapping;

    new_mapping = g_slice_dup(PopplerFormFieldMapping, mapping);

    if (mapping->field) {
        new_mapping->field = (PopplerFormField *)g_object_ref(mapping->field);
    }

    return new_mapping;
}

/**
 * poppler_form_field_mapping_free:
 * @mapping: a #PopplerFormFieldMapping
 *
 * Frees the given #PopplerFormFieldMapping
 */
void poppler_form_field_mapping_free(PopplerFormFieldMapping *mapping)
{
    if (G_UNLIKELY(!mapping)) {
        return;
    }

    if (mapping->field) {
        g_object_unref(mapping->field);
    }

    g_slice_free(PopplerFormFieldMapping, mapping);
}

/* PopplerAnnot Mapping Type */
G_DEFINE_BOXED_TYPE(PopplerAnnotMapping, poppler_annot_mapping, poppler_annot_mapping_copy, poppler_annot_mapping_free)

/**
 * poppler_annot_mapping_new:
 *
 * Creates a new #PopplerAnnotMapping
 *
 * Returns: a new #PopplerAnnotMapping, use poppler_annot_mapping_free() to free it
 */
PopplerAnnotMapping *poppler_annot_mapping_new(void)
{
    return g_slice_new0(PopplerAnnotMapping);
}

/**
 * poppler_annot_mapping_copy:
 * @mapping: a #PopplerAnnotMapping to copy
 *
 * Creates a copy of @mapping
 *
 * Returns: a new allocated copy of @mapping
 */
PopplerAnnotMapping *poppler_annot_mapping_copy(PopplerAnnotMapping *mapping)
{
    PopplerAnnotMapping *new_mapping;

    new_mapping = g_slice_dup(PopplerAnnotMapping, mapping);

    if (mapping->annot) {
        new_mapping->annot = (PopplerAnnot *)g_object_ref(mapping->annot);
    }

    return new_mapping;
}

/**
 * poppler_annot_mapping_free:
 * @mapping: a #PopplerAnnotMapping
 *
 * Frees the given #PopplerAnnotMapping
 */
void poppler_annot_mapping_free(PopplerAnnotMapping *mapping)
{
    if (G_UNLIKELY(!mapping)) {
        return;
    }

    if (mapping->annot) {
        g_object_unref(mapping->annot);
    }

    g_slice_free(PopplerAnnotMapping, mapping);
}

/**
 * poppler_page_get_crop_box:
 * @page: a #PopplerPage
 * @rect: (out): a #PopplerRectangle to fill
 *
 * Retrurns the crop box of @page
 */
void poppler_page_get_crop_box(PopplerPage *page, PopplerRectangle *rect)
{
    const PDFRectangle *cropBox = page->page->getCropBox();

    rect->x1 = cropBox->x1;
    rect->x2 = cropBox->x2;
    rect->y1 = cropBox->y1;
    rect->y2 = cropBox->y2;
}

/*
 * poppler_page_get_bounding_box:
 * @page: A #PopplerPage
 * @rect: (out) return the bounding box of the page
 *
 * Returns the bounding box of the page, a rectangle enclosing all text, vector
 * graphics (lines, rectangles and curves) and raster images in the page.
 * Includes invisible text but not (yet) annotations like highlights and form
 * elements.
 *
 * Return value: %TRUE if the page contains graphics, %FALSE otherwise
 *
 * Since: 0.88
 */
gboolean poppler_page_get_bounding_box(PopplerPage *page, PopplerRectangle *rect)
{
    BBoxOutputDev *bb_out;
    bool hasGraphics;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), false);
    g_return_val_if_fail(rect != nullptr, false);

    bb_out = new BBoxOutputDev();

    page->page->displaySlice(bb_out, 72.0, 72.0, 0, false, /* useMediaBox */
                             true, /* Crop */
                             -1, -1, -1, -1, false, /* printing */
                             nullptr, nullptr, nullptr, nullptr);
    hasGraphics = bb_out->getHasGraphics();
    if (hasGraphics) {
        rect->x1 = bb_out->getX1();
        rect->y1 = bb_out->getY1();
        rect->x2 = bb_out->getX2();
        rect->y2 = bb_out->getY2();
    }

    delete bb_out;
    return hasGraphics;
}

/**
 * poppler_page_get_text_layout:
 * @page: A #PopplerPage
 * @rectangles: (out) (array length=n_rectangles) (transfer container): return location for an array of #PopplerRectangle
 * @n_rectangles: (out): length of returned array
 *
 * Obtains the layout of the text as a list of #PopplerRectangle
 * This array must be freed with g_free() when done.
 *
 * The position in the array represents an offset in the text returned by
 * poppler_page_get_text()
 *
 * See also poppler_page_get_text_layout_for_area().
 *
 * Return value: %TRUE if the page contains text, %FALSE otherwise
 *
 * Since: 0.16
 **/
gboolean poppler_page_get_text_layout(PopplerPage *page, PopplerRectangle **rectangles, guint *n_rectangles)
{
    PopplerRectangle selection = { 0, 0, 0, 0 };

    g_return_val_if_fail(POPPLER_IS_PAGE(page), FALSE);

    poppler_page_get_size(page, &selection.x2, &selection.y2);

    return poppler_page_get_text_layout_for_area(page, &selection, rectangles, n_rectangles);
}

/**
 * poppler_page_get_text_layout_for_area:
 * @page: A #PopplerPage
 * @area: a #PopplerRectangle
 * @rectangles: (out) (array length=n_rectangles) (transfer container): return location for an array of #PopplerRectangle
 * @n_rectangles: (out): length of returned array
 *
 * Obtains the layout of the text contained in @area as a list of #PopplerRectangle
 * This array must be freed with g_free() when done.
 *
 * The position in the array represents an offset in the text returned by
 * poppler_page_get_text_for_area()
 *
 * Return value: %TRUE if the page contains text, %FALSE otherwise
 *
 * Since: 0.26
 **/
gboolean poppler_page_get_text_layout_for_area(PopplerPage *page, PopplerRectangle *area, PopplerRectangle **rectangles, guint *n_rectangles)
{
    TextPage *text;
    PopplerRectangle *rect;
    PDFRectangle selection;
    int i, k;
    guint offset = 0;
    guint n_rects = 0;
    gdouble x1, y1, x2, y2;
    gdouble x3, y3, x4, y4;
    int n_lines;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), FALSE);
    g_return_val_if_fail(area != nullptr, FALSE);

    *n_rectangles = 0;

    selection.x1 = area->x1;
    selection.y1 = area->y1;
    selection.x2 = area->x2;
    selection.y2 = area->y2;

    text = poppler_page_get_text_page(page);
    std::vector<TextWordSelection *> **word_list = text->getSelectionWords(&selection, selectionStyleGlyph, &n_lines);
    if (!word_list) {
        return FALSE;
    }

    n_rects += n_lines - 1;
    for (i = 0; i < n_lines; i++) {
        std::vector<TextWordSelection *> *line_words = word_list[i];
        n_rects += line_words->size() - 1;
        for (std::size_t j = 0; j < line_words->size(); j++) {
            const TextWordSelection *word_sel = (*line_words)[j];
            n_rects += word_sel->getEnd() - word_sel->getBegin();
            if (!word_sel->getWord()->hasSpaceAfter() && j < line_words->size() - 1) {
                n_rects--;
            }
        }
    }

    *rectangles = g_new(PopplerRectangle, n_rects);
    *n_rectangles = n_rects;

    for (i = 0; i < n_lines; i++) {
        std::vector<TextWordSelection *> *line_words = word_list[i];
        for (std::size_t j = 0; j < line_words->size(); j++) {
            TextWordSelection *word_sel = (*line_words)[j];
            const TextWord *word = word_sel->getWord();
            int end = word_sel->getEnd();

            for (k = word_sel->getBegin(); k < end; k++) {
                rect = *rectangles + offset;
                word->getCharBBox(k, &(rect->x1), &(rect->y1), &(rect->x2), &(rect->y2));
                offset++;
            }

            rect = *rectangles + offset;
            word->getBBox(&x1, &y1, &x2, &y2);

            if (word->hasSpaceAfter() && j < line_words->size() - 1) {
                TextWordSelection *next_word_sel = (*line_words)[j + 1];

                next_word_sel->getWord()->getBBox(&x3, &y3, &x4, &y4);
                // space is from one word to other and with the same height as
                // first word.
                rect->x1 = x2;
                rect->y1 = y1;
                rect->x2 = x3;
                rect->y2 = y2;
                offset++;
            }

            delete word_sel;
        }

        if (i < n_lines - 1 && offset > 0) {
            // end of line
            rect->x1 = x2;
            rect->y1 = y2;
            rect->x2 = x2;
            rect->y2 = y2;
            offset++;
        }

        delete line_words;
    }

    gfree(word_list);

    return TRUE;
}

/**
 * poppler_page_free_text_attributes:
 * @list: (element-type PopplerTextAttributes): A list of
 *   #PopplerTextAttributes<!-- -->s
 *
 * Frees a list of #PopplerTextAttributes<!-- -->s allocated by
 * poppler_page_get_text_attributes().
 *
 * Since: 0.18
 **/
void poppler_page_free_text_attributes(GList *list)
{
    if (G_UNLIKELY(list == nullptr)) {
        return;
    }

    g_list_free_full(list, (GDestroyNotify)poppler_text_attributes_free);
}

static gboolean word_text_attributes_equal(const TextWord *a, gint ai, const TextWord *b, gint bi)
{
    double ar, ag, ab, br, bg, bb;

    if (!a->getFontInfo(ai)->matches(b->getFontInfo(bi))) {
        return FALSE;
    }

    if (a->getFontSize() != b->getFontSize()) {
        return FALSE;
    }

    if (a->isUnderlined() != b->isUnderlined()) {
        return FALSE;
    }

    a->getColor(&ar, &ag, &ab);
    b->getColor(&br, &bg, &bb);
    return (ar == br && ag == bg && ab == bb);
}

/**
 * poppler_page_get_text_attributes:
 * @page: A #PopplerPage
 *
 * Obtains the attributes of the text as a #GList of #PopplerTextAttributes.
 * This list must be freed with poppler_page_free_text_attributes() when done.
 *
 * Each list element is a #PopplerTextAttributes struct where start_index and
 * end_index indicates the range of text (as returned by poppler_page_get_text())
 * to which text attributes apply.
 *
 * See also poppler_page_get_text_attributes_for_area()
 *
 * Return value: (element-type PopplerTextAttributes) (transfer full): A #GList of #PopplerTextAttributes
 *
 * Since: 0.18
 **/
GList *poppler_page_get_text_attributes(PopplerPage *page)
{
    PopplerRectangle selection = { 0, 0, 0, 0 };

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);

    poppler_page_get_size(page, &selection.x2, &selection.y2);

    return poppler_page_get_text_attributes_for_area(page, &selection);
}

/**
 * poppler_page_get_text_attributes_for_area:
 * @page: A #PopplerPage
 * @area: a #PopplerRectangle
 *
 * Obtains the attributes of the text in @area as a #GList of #PopplerTextAttributes.
 * This list must be freed with poppler_page_free_text_attributes() when done.
 *
 * Each list element is a #PopplerTextAttributes struct where start_index and
 * end_index indicates the range of text (as returned by poppler_page_get_text_for_area())
 * to which text attributes apply.
 *
 * Return value: (element-type PopplerTextAttributes) (transfer full): A #GList of #PopplerTextAttributes
 *
 * Since: 0.26
 **/
GList *poppler_page_get_text_attributes_for_area(PopplerPage *page, PopplerRectangle *area)
{
    TextPage *text;
    PDFRectangle selection;
    int n_lines;
    PopplerTextAttributes *attrs = nullptr;
    const TextWord *word, *prev_word = nullptr;
    gint word_i, prev_word_i;
    gint i;
    gint offset = 0;
    GList *attributes = nullptr;

    g_return_val_if_fail(POPPLER_IS_PAGE(page), NULL);
    g_return_val_if_fail(area != nullptr, nullptr);

    selection.x1 = area->x1;
    selection.y1 = area->y1;
    selection.x2 = area->x2;
    selection.y2 = area->y2;

    text = poppler_page_get_text_page(page);
    std::vector<TextWordSelection *> **word_list = text->getSelectionWords(&selection, selectionStyleGlyph, &n_lines);
    if (!word_list) {
        return nullptr;
    }

    for (i = 0; i < n_lines; i++) {
        std::vector<TextWordSelection *> *line_words = word_list[i];
        for (std::size_t j = 0; j < line_words->size(); j++) {
            TextWordSelection *word_sel = (*line_words)[j];
            int end = word_sel->getEnd();

            word = word_sel->getWord();

            for (word_i = word_sel->getBegin(); word_i < end; word_i++) {
                if (!prev_word || !word_text_attributes_equal(word, word_i, prev_word, prev_word_i)) {
                    attrs = poppler_text_attributes_new_from_word(word, word_i);
                    attrs->start_index = offset;
                    attributes = g_list_prepend(attributes, attrs);
                }
                attrs->end_index = offset;
                offset++;
                prev_word = word;
                prev_word_i = word_i;
            }

            if (word->hasSpaceAfter() && j < line_words->size() - 1) {
                attrs->end_index = offset;
                offset++;
            }

            delete word_sel;
        }

        if (i < n_lines - 1) {
            attrs->end_index = offset;
            offset++;
        }

        delete line_words;
    }

    gfree(word_list);

    return g_list_reverse(attributes);
}
