//========================================================================
//
// CairoOutputDev.cc
//
// Copyright 2003 Glyph & Cog, LLC
// Copyright 2004 Red Hat, Inc
//
//========================================================================

//========================================================================
//
// Modified under the Poppler project - http://poppler.freedesktop.org
//
// All changes made under the Poppler project to this file are licensed
// under GPL version 2 or later
//
// Copyright (C) 2005-2008 Jeff Muizelaar <jeff@infidigm.net>
// Copyright (C) 2005, 2006 Kristian Høgsberg <krh@redhat.com>
// Copyright (C) 2005, 2009, 2012, 2017-2021, 2023-2026 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2005 Nickolay V. Shmyrev <nshmyrev@yandex.ru>
// Copyright (C) 2006-2011, 2013, 2014, 2017, 2018 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2008 Carl Worth <cworth@cworth.org>
// Copyright (C) 2008-2018, 2021-2024 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2008 Michael Vrable <mvrable@cs.ucsd.edu>
// Copyright (C) 2008, 2009 Chris Wilson <chris@chris-wilson.co.uk>
// Copyright (C) 2008, 2012 Hib Eris <hib@hiberis.nl>
// Copyright (C) 2009, 2010 David Benjamin <davidben@mit.edu>
// Copyright (C) 2011-2014 Thomas Freitag <Thomas.Freitag@alfa.de>
// Copyright (C) 2012 Patrick Pfeifer <p2000@mailinator.com>
// Copyright (C) 2012, 2015, 2016 Jason Crain <jason@aquaticape.us>
// Copyright (C) 2015 Suzuki Toshiya <mpsuzuki@hiroshima-u.ac.jp>
// Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich
// Copyright (C) 2018, 2020 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2019, 2020, 2022 Marek Kasik <mkasik@redhat.com>
// Copyright (C) 2020 Michal <sudolskym@gmail.com>
// Copyright (C) 2020, 2022 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2021 Uli Schlachter <psychon@znc.in>
// Copyright (C) 2021 Christian Persch <chpe@src.gnome.org>
// Copyright (C) 2022 Zachary Travis <ztravis@everlaw.com>
// Copyright (C) 2023 Artemy Gordon <artemy.gordon@gmail.com>
// Copyright (C) 2023 Anton Thomasson <antonthomasson@gmail.com>
// Copyright (C) 2024 Vincent Lefevre <vincent@vinc17.net>
// Copyright (C) 2024 Athul Raj Kollareth <krathul3152@gmail.com>
// Copyright (C) 2024, 2025 Nelson Benítez León <nbenitezl@gmail.com>
// Copyright (C) 2025, 2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
// Copyright (C) 2025 Jonathan Hähne <jonathan.haehne@hotmail.com>
// Copyright (C) 2025 Arnav V <arnav0872@gmail.com>
// Copyright (C) 2026 Stefan Brüns <stefan.bruens@rwth-aachen.de>
//
// To see a description of the changes please see the Changelog file that
// came with your tarball or type make ChangeLog if you are building from git
//
//========================================================================

#include <config.h>

#include <cstdint>
#include <cstring>
#include <cmath>
#include <cassert>
#include <cairo.h>

#include "GlobalParams.h"
#include "Error.h"
#include "Object.h"
#include "Gfx.h"
#include "GfxState.h"
#include "GfxFont.h"
#include "Page.h"
#include "Link.h"
#include <fofi/FoFiTrueType.h>
#include <goo/gmem.h>
#include <splash/SplashBitmap.h>
#include "CairoOutputDev.h"
#include "CairoFontEngine.h"
#include "CairoRescaleBox.h"
#include "UnicodeMap.h"
#include "UTF.h"
#include "JBIG2Stream.h"
//------------------------------------------------------------------------

// #define LOG_CAIRO

// To limit memory usage and improve performance when printing, limit
// cairo images to this size. 8192 is sufficient for an A2 sized
// 300ppi image.
constexpr int MAX_PRINT_IMAGE_SIZE = 8192;
// Cairo has a max size for image surfaces due to their fixed-point
// coordinate handling, namely INT16_MAX, aka 32767.
constexpr int MAX_CAIRO_IMAGE_SIZE = 32767;

#ifdef LOG_CAIRO
#    define LOG(x) (x)
#else
#    define LOG(x)
#endif

#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))

//------------------------------------------------------------------------
// CairoImage
//------------------------------------------------------------------------

CairoImage::CairoImage(double x1A, double y1A, double x2A, double y2A)
{
    image = nullptr;
    x1 = x1A;
    y1 = y1A;
    x2 = x2A;
    y2 = y2A;
}

CairoImage::~CairoImage()
{
    if (image) {
        cairo_surface_destroy(image);
    }
}

void CairoImage::setImage(cairo_surface_t *i)
{
    if (image) {
        cairo_surface_destroy(image);
    }
    image = cairo_surface_reference(i);
}

//------------------------------------------------------------------------
// CairoOutputDev
//------------------------------------------------------------------------

// We cannot tie the lifetime of an FT_Library object to that of
// CairoOutputDev, since any FT_Faces created with it may end up with a
// reference by Cairo which can be held long after the CairoOutputDev is
// deleted.  The simplest way to avoid problems is to never tear down the
// FT_Library instance; to avoid leaks, just use a single global instance
// initialized the first time it is needed.
FT_Library CairoOutputDev::ft_lib;
std::once_flag CairoOutputDev::ft_lib_once_flag;

CairoOutputDev::CairoOutputDev()
{
    doc = nullptr;

    std::call_once(ft_lib_once_flag, FT_Init_FreeType, &ft_lib);

    fontEngine = nullptr;
    fontEngine_owner = false;
    glyphs = nullptr;
    fill_pattern = nullptr;
    fill_color = {};
    stroke_pattern = nullptr;
    stroke_color = {};
    stroke_opacity = 1.0;
    fill_opacity = 1.0;
    textClipPath = nullptr;
    strokePathClip = nullptr;
    cairo = nullptr;
    currentFont = nullptr;
    printing = true;
    use_show_text_glyphs = false;
    inUncoloredPattern = false;
    t3_render_state = Type3RenderNone;
    t3_glyph_has_bbox = false;
    t3_glyph_has_color = false;
    text_matrix_valid = true;

    groupColorSpaceStack = nullptr;
    group = nullptr;
    mask = nullptr;
    shape = nullptr;
    cairo_shape = nullptr;
    knockoutCount = 0;

    textPage = nullptr;
    actualText = nullptr;
    logicalStruct = false;
    pdfPageNum = 0;
    cairoPageNum = 0;

    // the SA parameter supposedly defaults to false, but Acrobat
    // apparently hardwires it to true
    stroke_adjust = true;
    align_stroke_coords = false;
    adjusted_stroke_width = false;
    xref = nullptr;
    currentStructParents = -1;
}

CairoOutputDev::~CairoOutputDev()
{
    if (fontEngine_owner && fontEngine) {
        delete fontEngine;
    }
    if (textClipPath) {
        cairo_path_destroy(textClipPath);
        textClipPath = nullptr;
    }

    if (cairo) {
        cairo_destroy(cairo);
    }
    cairo_pattern_destroy(stroke_pattern);
    cairo_pattern_destroy(fill_pattern);
    if (group) {
        cairo_pattern_destroy(group);
    }
    if (mask) {
        cairo_pattern_destroy(mask);
    }
    if (shape) {
        cairo_pattern_destroy(shape);
    }
}

void CairoOutputDev::setCairo(cairo_t *c)
{
    if (cairo != nullptr) {
        cairo_status_t status = cairo_status(cairo);
        if (status) {
            error(errInternal, -1, "cairo context error: {0:s}", cairo_status_to_string(status));
        }
        cairo_destroy(cairo);
        assert(!cairo_shape);
    }
    if (c != nullptr) {
        cairo = cairo_reference(c);
        /* save the initial matrix so that we can use it for type3 fonts. */
        // XXX: is this sufficient? could we miss changes to the matrix somehow?
        cairo_get_matrix(cairo, &orig_matrix);
    } else {
        cairo = nullptr;
        cairo_shape = nullptr;
    }
}

bool CairoOutputDev::isPDF()
{
    if (cairo) {
        return cairo_surface_get_type(cairo_get_target(cairo)) == CAIRO_SURFACE_TYPE_PDF;
    }
    return false;
}

void CairoOutputDev::setTextPage(std::shared_ptr<TextPage> text)
{
    actualText.reset();
    textPage = std::move(text);
    if (textPage) {
        actualText = std::make_unique<ActualText>(textPage.get());
    }
}

void CairoOutputDev::copyAntialias(cairo_t *cr, cairo_t *source_cr)
{
    cairo_set_antialias(cr, cairo_get_antialias(source_cr));

    cairo_font_options_t *font_options = cairo_font_options_create();
    cairo_get_font_options(source_cr, font_options);
    cairo_set_font_options(cr, font_options);
    cairo_font_options_destroy(font_options);
}

void CairoOutputDev::startDoc(PDFDoc *docA, CairoFontEngine *parentFontEngine)
{
    doc = docA;
    if (parentFontEngine) {
        fontEngine = parentFontEngine;
    } else {
        delete fontEngine;
        fontEngine = new CairoFontEngine(ft_lib);
        fontEngine_owner = true;
    }
    xref = doc->getXRef();

    mcidEmitted.clear();
    destsMap.clear();
    emittedDestinations.clear();
    pdfPageToCairoPageMap.clear();
    pdfPageRefToCairoPageNumMap.clear();
    cairoPageNum = 0;
    firstPage = true;
}

void CairoOutputDev::textStringToQuotedUtf8(const GooString *text, GooString *s)
{
    std::string utf8 = TextStringToUtf8(text->toStr());
    s->assign("'");
    for (char c : utf8) {
        if (c == '\\' || c == '\'') {
            s->append("\\");
        }
        s->push_back(c);
    }
    s->append("'");
}

// Initialization that needs to be performed after setCairo() is called.
void CairoOutputDev::startFirstPage(XRef *xrefA)
{
    if (xrefA) {
        xref = xrefA;
    }

    if (logicalStruct && isPDF()) {
        int numDests = doc->getCatalog()->numDestNameTree();
        for (int i = 0; i < numDests; i++) {
            const GooString *name = doc->getCatalog()->getDestNameTreeName(i);
            std::unique_ptr<LinkDest> dest = doc->getCatalog()->getDestNameTreeDest(i);
            if (dest->isPageRef()) {
                Ref ref = dest->getPageRef();
                destsMap[ref].insert({ std::string(name->toStr()), std::move(dest) });
            }
        }

        numDests = doc->getCatalog()->numDests();
        for (int i = 0; i < numDests; i++) {
            const char *name = doc->getCatalog()->getDestsName(i);
            std::unique_ptr<LinkDest> dest = doc->getCatalog()->getDestsDest(i);
            if (dest->isPageRef()) {
                Ref ref = dest->getPageRef();
                destsMap[ref].insert({ std::string(name), std::move(dest) });
            }
        }
    }
}

void CairoOutputDev::startPage(int pageNum, GfxState *state, XRef *xrefA)
{
    if (firstPage) {
        startFirstPage(xrefA);
        firstPage = false;
    }

    /* set up some per page defaults */
    cairo_pattern_destroy(fill_pattern);
    cairo_pattern_destroy(stroke_pattern);

    fill_pattern = cairo_pattern_create_rgb(0., 0., 0.);
    fill_color = { .r = 0, .g = 0, .b = 0 };
    stroke_pattern = cairo_pattern_reference(fill_pattern);
    stroke_color = { .r = 0, .g = 0, .b = 0 };

    if (textPage) {
        textPage->startPage(state);
    }

    pdfPageNum = pageNum;
    cairoPageNum++;
    pdfPageToCairoPageMap[pdfPageNum] = cairoPageNum;

    if (logicalStruct && isPDF()) {
        Object obj = doc->getPage(pageNum)->getAnnotsObject(xref);
        auto *annots = new Annots(doc, pageNum, &obj);

        for (const std::shared_ptr<Annot> &annot : annots->getAnnots()) {
            if (annot->getType() == Annot::typeLink) {
                annotations.push_back(annot);
            }
        }

        delete annots;

        // emit dests
        Ref *ref = doc->getCatalog()->getPageRef(pageNum);
        pdfPageRefToCairoPageNumMap[*ref] = cairoPageNum;
        auto pageDests = destsMap.find(*ref);
        if (pageDests != destsMap.end()) {
            for (auto &it : pageDests->second) {
                GooString quoted_name;
                GooString name(it.first);
                textStringToQuotedUtf8(&name, &quoted_name);
                emittedDestinations.insert(quoted_name.toStr());

                std::string attrib = GooString::format("name={0:t} ", &quoted_name);
                if (it.second->getChangeLeft()) {
                    GooString::appendf(attrib, "x={0:g} ", it.second->getLeft());
                }
                if (it.second->getChangeTop()) {
                    GooString::appendf(attrib, "y={0:g} ", state->getPageHeight() - it.second->getTop());
                }

                cairo_tag_begin(cairo, CAIRO_TAG_DEST, attrib.c_str());
                cairo_tag_end(cairo, CAIRO_TAG_DEST);
            }
        }

        currentStructParents = doc->getPage(pageNum)->getStructParents();
    }
}

void CairoOutputDev::endPage()
{
    if (textPage) {
        textPage->endPage();
        textPage->coalesce(true, 0, false);
    }
}

void CairoOutputDev::beginForm(Object *obj, Ref /*id*/)
{
    if (logicalStruct && isPDF()) {
        structParentsStack.push_back(currentStructParents);

        const Object tmp = obj->getStream()->getDict()->lookup("StructParents");
        if (!(tmp.isInt() || tmp.isNull())) {
            error(errSyntaxError, -1, "XObject StructParents object is wrong type ({0:s})", tmp.getTypeName());
        } else if (tmp.isInt()) {
            currentStructParents = tmp.getInt();
        }
    }
}

void CairoOutputDev::endForm(Object * /*obj*/, Ref /*id*/)
{
    if (logicalStruct && isPDF()) {
        currentStructParents = structParentsStack.back();
        structParentsStack.pop_back();
    }
}

void CairoOutputDev::quadToCairoRect(const AnnotQuadrilaterals *quads, int idx, double pageHeight, cairo_rectangle_t *rect)
{
    double x1, x2, y1, y2;
    x1 = x2 = quads->getX1(idx);
    y1 = y2 = quads->getX2(idx);

    x1 = std::min(x1, quads->getX2(idx));
    x1 = std::min(x1, quads->getX3(idx));
    x1 = std::min(x1, quads->getX4(idx));

    y1 = std::min(y1, quads->getY2(idx));
    y1 = std::min(y1, quads->getY3(idx));
    y1 = std::min(y1, quads->getY4(idx));

    x2 = std::max(x2, quads->getX2(idx));
    x2 = std::max(x2, quads->getX3(idx));
    x2 = std::max(x2, quads->getX4(idx));

    y2 = std::max(y2, quads->getY2(idx));
    y2 = std::max(y2, quads->getY3(idx));
    y2 = std::max(y2, quads->getY4(idx));

    rect->x = x1;
    rect->y = pageHeight - y2;
    rect->width = x2 - x1;
    rect->height = y2 - y1;
}

bool CairoOutputDev::appendLinkDestRef(std::string &s, const LinkDest *dest)
{
    Ref ref = dest->getPageRef();
    auto pageNum = pdfPageRefToCairoPageNumMap.find(ref);
    if (pageNum != pdfPageRefToCairoPageNumMap.end()) {
        auto cairoPage = pdfPageToCairoPageMap.find(pageNum->second);
        if (cairoPage != pdfPageToCairoPageMap.end()) {
            GooString::appendf(s, "page={0:d} ", cairoPage->second);
            double destPageHeight = doc->getPageMediaHeight(dest->getPageNum());
            appendLinkDestXY(s, dest, destPageHeight);
            return true;
        }
    }
    return false;
}

void CairoOutputDev::appendLinkDestXY(std::string &s, const LinkDest *dest, double destPageHeight)
{
    double x = 0;
    double y = 0;

    if (dest->getChangeLeft()) {
        x = dest->getLeft();
    }

    if (dest->getChangeTop()) {
        y = dest->getTop();
    }

    // if pageHeight is 0, dest is remote document, cairo uses PDF coords in this
    // case. So don't flip coords when pageHeight is 0.
    GooString::appendf(s, "pos=[{0:g} {1:g}] ", x, destPageHeight ? destPageHeight - y : y);
}

bool CairoOutputDev::beginLinkTag(AnnotLink *annotLink)
{
    int page_num = annotLink->getPageNum();
    double height = doc->getPageMediaHeight(page_num);

    auto attrib = GooString::format("link_page={0:d} ", page_num);
    attrib.append("rect=[");
    const AnnotQuadrilaterals *quads = annotLink->getQuadrilaterals();
    if (quads && quads->getQuadrilateralsLength() > 0) {
        for (int i = 0; i < quads->getQuadrilateralsLength(); i++) {
            cairo_rectangle_t rect;
            quadToCairoRect(quads, i, height, &rect);
            GooString::appendf(attrib, "{0:g} {1:g} {2:g} {3:g} ", rect.x, rect.y, rect.width, rect.height);
        }
    } else {
        double x1, x2, y1, y2;
        annotLink->getRect(&x1, &y1, &x2, &y2);
        GooString::appendf(attrib, "{0:g} {1:g} {2:g} {3:g} ", x1, height - y2, x2 - x1, y2 - y1);
    }
    attrib.append("] ");

    LinkAction *action = annotLink->getAction();
    if (action->getKind() == actionGoTo) {
        auto *act = static_cast<LinkGoTo *>(action);
        if (act->isOk()) {
            const GooString *namedDest = act->getNamedDest();
            const LinkDest *linkDest = act->getDest();
            if (namedDest) {
                GooString name;
                textStringToQuotedUtf8(namedDest, &name);
                if (!emittedDestinations.contains(name.toStr())) {
                    return false;
                }
                GooString::appendf(attrib, "dest={0:t} ", &name);
            } else if (linkDest && linkDest->isOk() && linkDest->isPageRef()) {
                bool ok = appendLinkDestRef(attrib, linkDest);
                if (!ok) {
                    return false;
                }
            }
        }
    } else if (action->getKind() == actionGoToR) {
        auto *act = static_cast<LinkGoToR *>(action);
        GooString::appendf(attrib, "file='{0:t}' ", act->getFileName());
        const GooString *namedDest = act->getNamedDest();
        const LinkDest *linkDest = act->getDest();
        if (namedDest) {
            GooString name;
            textStringToQuotedUtf8(namedDest, &name);
            if (!emittedDestinations.contains(name.toStr())) {
                return false;
            }
            GooString::appendf(attrib, "dest={0:t} ", &name);
        } else if (linkDest && linkDest->isOk() && !linkDest->isPageRef()) {
            auto cairoPage = pdfPageToCairoPageMap.find(linkDest->getPageNum());
            if (cairoPage != pdfPageToCairoPageMap.end()) {
                GooString::appendf(attrib, "page={0:d} ", cairoPage->second);
                appendLinkDestXY(attrib, linkDest, 0.0);
            } else {
                return false;
            }
        }
    } else if (action->getKind() == actionURI) {
        auto *act = static_cast<LinkURI *>(action);
        if (act->isOk()) {
            GooString::appendf(attrib, "uri='{0:s}'", act->getURI().c_str());
        }
    }
    cairo_tag_begin(cairo, CAIRO_TAG_LINK, attrib.c_str());
    return true;
}

AnnotLink *CairoOutputDev::findLinkObject(const StructElement *elem)
{
    if (elem->isObjectRef()) {
        Ref ref = elem->getObjectRef();
        for (const std::shared_ptr<Annot> &annot : annotations) {
            if (annot->getType() == Annot::typeLink && annot->match(ref)) {
                return static_cast<AnnotLink *>(annot.get());
            }
        }
    }

    for (unsigned i = 0; i < elem->getNumChildren(); i++) {
        AnnotLink *link = findLinkObject(elem->getChild(i));
        if (link) {
            return link;
        }
    }

    return nullptr;
}

bool CairoOutputDev::beginLink(const StructElement *linkElem)
{
    bool emitted = true;
    AnnotLink *linkAnnot = findLinkObject(linkElem);
    if (linkAnnot) {
        emitted = beginLinkTag(linkAnnot);
    } else {
        cairo_tag_begin(cairo, linkElem->getTypeName(), nullptr);
    }
    return emitted;
}

void CairoOutputDev::getStructElemAttributeString(const StructElement *elem)
{
    (void)elem;
#if 0
    int mcid = 0;
    GooString attribs;
    Ref ref = elem->getObjectRef();
    attribs.appendf("id='{0:d}_{1:d}_{2:d}'", ref.num, ref.gen, mcid);
    attribs.appendf(" parent='{0:d}_{1:d}'", ref.num, ref.gen);
#endif
}

int CairoOutputDev::getContentElementStructParents(const StructElement *element)
{
    int structParents = -1;
    Ref ref;

    if (element->hasStmRef()) {
        element->getStmRef(ref);
        Object xobjectObj = xref->fetch(ref);
        const Object &spObj = xobjectObj.getStream()->getDict()->lookup("StructParents");
        if (spObj.isInt()) {
            structParents = spObj.getInt();
        }
    } else if (element->hasPageRef()) {
        element->getPageRef(ref);
        Object pageObj = xref->fetch(ref);
        const Object &spObj = pageObj.dictLookup("StructParents");
        if (spObj.isInt()) {
            structParents = spObj.getInt();
        }
    }

    if (structParents == -1) {
        error(errSyntaxError, -1, "Unable to find StructParents object for StructElement");
    }
    return structParents;
}

bool CairoOutputDev::checkIfStructElementNeeded(const StructElement *element)
{
    if (element->isContent() && !element->isObjectRef()) {
        int structParents = getContentElementStructParents(element);
        int mcid = element->getMCID();
        if (mcidEmitted.contains(std::pair(structParents, mcid))) {
            structElementNeeded.insert(element);
            return true;
        }
    } else if (!element->isContent()) {
        bool needed = false;
        for (unsigned i = 0; i < element->getNumChildren(); i++) {
            if (checkIfStructElementNeeded(element->getChild(i))) {
                needed = true;
            }
        }
        if (needed) {
            structElementNeeded.insert(element);
        }
        return needed;
    }
    return false;
}

void CairoOutputDev::emitStructElement(const StructElement *element)
{
    if (!structElementNeeded.contains(element)) {
        return;
    }

    if (element->isContent() && !element->isObjectRef()) {
        int structParents = getContentElementStructParents(element);
        int mcid = element->getMCID();
        std::string attribs = GooString::format("ref='{0:d}_{1:d}'", structParents, mcid);
        cairo_tag_begin(cairo, CAIRO_TAG_CONTENT_REF, attribs.c_str());
        cairo_tag_end(cairo, CAIRO_TAG_CONTENT_REF);
    } else if (!element->isContent()) {
        if (element->getType() == StructElement::Link) {
            bool ok = beginLink(element);
            if (!ok) {
                return;
            }
        } else {
            cairo_tag_begin(cairo, element->getTypeName(), "");
        }
        for (unsigned i = 0; i < element->getNumChildren(); i++) {
            emitStructElement(element->getChild(i));
        }
        cairo_tag_end(cairo, element->getTypeName());
    }
}

void CairoOutputDev::emitStructTree()
{
    if (logicalStruct && isPDF()) {
        const StructTreeRoot *root = doc->getStructTreeRoot();
        if (!root) {
            return;
        }

        for (unsigned i = 0; i < root->getNumChildren(); i++) {
            checkIfStructElementNeeded(root->getChild(i));
        }

        for (unsigned i = 0; i < root->getNumChildren(); i++) {
            emitStructElement(root->getChild(i));
        }
    }
}

void CairoOutputDev::startType3Render(XRef *xrefA)
{
    /* When cairo calls a user font render function, the default
     * source set on the provided cairo_t must be used, except in the
     * case of a color user font explicitly setting a color.
     *
     * As startPage() resets the source to solid black, this function
     * is used instead to initialise the CairoOutputDev when rendering
     * a user font glyph.
     *
     * As noted in the Cairo documentation, the default source of a
     * render callback contains an internal marker denoting the
     * foreground color is to be used when the glyph is rendered, even
     * though querying the default source will reveal solid black.
     * For this reason, fill_color and stroke_color are set to nullopt
     * to ensure updateFillColor()/updateStrokeColor() will update the
     * color even if the new color is black.
     *
     * The saveState()/restoreState() functions also ensure the
     * default source is saved and restored, and the fill_color and
     * stroke_color is reset to nullopt for the same reason.
     */

    /* Initialise fill and stroke pattern to the current source pattern */
    fill_pattern = cairo_pattern_reference(cairo_get_source(cairo));
    stroke_pattern = cairo_pattern_reference(cairo_get_source(cairo));
    fill_color = {};
    stroke_color = {};
    t3_glyph_has_bbox = false;
    t3_glyph_has_color = false;

    if (xrefA != nullptr) {
        xref = xrefA;
    }
}

void CairoOutputDev::saveState(GfxState * /*state*/)
{
    LOG(printf("save\n"));
    cairo_save(cairo);
    if (cairo_shape) {
        cairo_save(cairo_shape);
    }

    /* To ensure the current source, potentially containing the hidden
     * foreground color maker, is saved and restored as required by
     * _render_type3_glyph, we avoid using the update color and
     * opacity functions in restoreState() and instead be careful to
     * save all the color related variables that have been set by the
     * update functions on the stack. */
    SaveStateElement elem;
    elem.fill_pattern = cairo_pattern_reference(fill_pattern);
    elem.fill_opacity = fill_opacity;
    elem.stroke_pattern = cairo_pattern_reference(stroke_pattern);
    elem.stroke_opacity = stroke_opacity;
    elem.mask = mask ? cairo_pattern_reference(mask) : nullptr;
    elem.mask_matrix = mask_matrix;
    elem.fontRef = currentFont ? currentFont->getRef() : Ref::INVALID();
    saveStateStack.push_back(elem);

    if (strokePathClip) {
        strokePathClip->ref_count++;
    }
}

void CairoOutputDev::restoreState(GfxState *state)
{
    LOG(printf("restore\n"));
    cairo_restore(cairo);
    if (cairo_shape) {
        cairo_restore(cairo_shape);
    }

    text_matrix_valid = true;

    cairo_pattern_destroy(fill_pattern);
    fill_pattern = saveStateStack.back().fill_pattern;
    fill_color = {};
    fill_opacity = saveStateStack.back().fill_opacity;

    cairo_pattern_destroy(stroke_pattern);
    stroke_pattern = saveStateStack.back().stroke_pattern;
    stroke_color = {};
    stroke_opacity = saveStateStack.back().stroke_opacity;

    if (saveStateStack.back().fontRef != (currentFont ? currentFont->getRef() : Ref::INVALID())) {
        needFontUpdate = true;
    }

    /* This isn't restored by cairo_restore() since we keep it in the
     * output device. */
    updateBlendMode(state);

    if (mask) {
        cairo_pattern_destroy(mask);
    }
    mask = saveStateStack.back().mask;
    mask_matrix = saveStateStack.back().mask_matrix;
    saveStateStack.pop_back();

    if (strokePathClip && --strokePathClip->ref_count == 0) {
        delete strokePathClip->path;
        if (strokePathClip->dashes) {
            gfree(strokePathClip->dashes);
        }
        gfree(strokePathClip);
        strokePathClip = nullptr;
    }
}

void CairoOutputDev::updateAll(GfxState *state)
{
    updateLineDash(state);
    updateLineJoin(state);
    updateLineCap(state);
    updateLineWidth(state);
    updateFlatness(state);
    updateMiterLimit(state);
    updateFillColor(state);
    updateStrokeColor(state);
    updateFillOpacity(state);
    updateStrokeOpacity(state);
    updateBlendMode(state);
    needFontUpdate = true;
    if (textPage) {
        textPage->updateFont(state);
    }
}

void CairoOutputDev::setDefaultCTM(const std::array<double, 6> &ctm)
{
    cairo_matrix_t matrix;
    matrix.xx = ctm[0];
    matrix.yx = ctm[1];
    matrix.xy = ctm[2];
    matrix.yy = ctm[3];
    matrix.x0 = ctm[4];
    matrix.y0 = ctm[5];

    cairo_transform(cairo, &matrix);
    if (cairo_shape) {
        cairo_transform(cairo_shape, &matrix);
    }

    OutputDev::setDefaultCTM(ctm);
}

void CairoOutputDev::updateCTM(GfxState *state, double m11, double m12, double m21, double m22, double m31, double m32)
{
    cairo_matrix_t matrix, invert_matrix;
    matrix.xx = m11;
    matrix.yx = m12;
    matrix.xy = m21;
    matrix.yy = m22;
    matrix.x0 = m31;
    matrix.y0 = m32;

    /* Make sure the matrix is invertible before setting it.
     * cairo will blow up if we give it a matrix that's not
     * invertible, so we need to check before passing it
     * to cairo_transform. Ignoring it is likely to give better
     * results than not rendering anything at all. See #14398
     *
     * Ideally, we could do the cairo_transform
     * and then check if anything went wrong and fix it then
     * instead of having to invert the matrix. */
    invert_matrix = matrix;
    if (cairo_matrix_invert(&invert_matrix)) {
        error(errSyntaxWarning, -1, "matrix not invertible");
        return;
    }

    cairo_transform(cairo, &matrix);
    if (cairo_shape) {
        cairo_transform(cairo_shape, &matrix);
    }
    updateLineDash(state);
    updateLineJoin(state);
    updateLineCap(state);
    updateLineWidth(state);
}

void CairoOutputDev::updateLineDash(GfxState *state)
{
    double dashStart;

    const std::vector<double> &dashPattern = state->getLineDash(&dashStart);
    cairo_set_dash(cairo, dashPattern.data(), dashPattern.size(), dashStart);
    if (cairo_shape) {
        cairo_set_dash(cairo_shape, dashPattern.data(), dashPattern.size(), dashStart);
    }
}

void CairoOutputDev::updateFlatness(GfxState * /*state*/)
{
    // cairo_set_tolerance (cairo, state->getFlatness());
}

void CairoOutputDev::updateLineJoin(GfxState *state)
{
    switch (state->getLineJoin()) {
    case GfxState::LineJoinMitre:
        cairo_set_line_join(cairo, CAIRO_LINE_JOIN_MITER);
        break;
    case GfxState::LineJoinRound:
        cairo_set_line_join(cairo, CAIRO_LINE_JOIN_ROUND);
        break;
    case GfxState::LineJoinBevel:
        cairo_set_line_join(cairo, CAIRO_LINE_JOIN_BEVEL);
        break;
    }
    if (cairo_shape) {
        cairo_set_line_join(cairo_shape, cairo_get_line_join(cairo));
    }
}

void CairoOutputDev::updateLineCap(GfxState *state)
{
    switch (state->getLineCap()) {
    case GfxState::LineCapButt:
        cairo_set_line_cap(cairo, CAIRO_LINE_CAP_BUTT);
        break;
    case GfxState::LineCapRound:
        cairo_set_line_cap(cairo, CAIRO_LINE_CAP_ROUND);
        break;
    case GfxState::LineCapProjecting:
        cairo_set_line_cap(cairo, CAIRO_LINE_CAP_SQUARE);
        break;
    }
    if (cairo_shape) {
        cairo_set_line_cap(cairo_shape, cairo_get_line_cap(cairo));
    }
}

void CairoOutputDev::updateMiterLimit(GfxState *state)
{
    cairo_set_miter_limit(cairo, state->getMiterLimit());
    if (cairo_shape) {
        cairo_set_miter_limit(cairo_shape, state->getMiterLimit());
    }
}

void CairoOutputDev::updateLineWidth(GfxState *state)
{
    LOG(printf("line width: %f\n", state->getLineWidth()));
    adjusted_stroke_width = false;
    double width = state->getLineWidth();
    if (stroke_adjust && !printing) {
        double x, y;
        x = y = width;

        /* find out line width in device units */
        cairo_user_to_device_distance(cairo, &x, &y);
        if (fabs(x) <= 1.0 && fabs(y) <= 1.0) {
            /* adjust width to at least one device pixel */
            x = y = 1.0;
            cairo_device_to_user_distance(cairo, &x, &y);
            width = MIN(fabs(x), fabs(y));
            adjusted_stroke_width = true;
        }
    } else if (width == 0.0) {
        /* Cairo does not support 0 line width == 1 device pixel. Find out
         * how big pixels (device unit) are in the x and y
         * directions. Choose the smaller of the two as our line width.
         */
        double x = 1.0, y = 1.0;
        if (printing) {
            // assume printer pixel size is 1/600 inch
            x = 72.0 / 600;
            y = 72.0 / 600;
        }
        cairo_device_to_user_distance(cairo, &x, &y);
        width = MIN(fabs(x), fabs(y));
    }
    cairo_set_line_width(cairo, width);
    if (cairo_shape) {
        cairo_set_line_width(cairo_shape, cairo_get_line_width(cairo));
    }
}

void CairoOutputDev::updateFillColor(GfxState *state)
{
    if (inUncoloredPattern) {
        return;
    }

    GfxRGB new_color;
    state->getFillRGB(&new_color);
    bool color_match = fill_color && *fill_color == new_color;
    if (cairo_pattern_get_type(fill_pattern) != CAIRO_PATTERN_TYPE_SOLID || !color_match) {
        cairo_pattern_destroy(fill_pattern);
        fill_pattern = cairo_pattern_create_rgba(colToDbl(new_color.r), colToDbl(new_color.g), colToDbl(new_color.b), fill_opacity);
        fill_color = new_color;
        LOG(printf("fill color: %d %d %d\n", fill_color->r, fill_color->g, fill_color->b));
    }
}

void CairoOutputDev::updateStrokeColor(GfxState *state)
{

    if (inUncoloredPattern) {
        return;
    }

    GfxRGB new_color;
    state->getStrokeRGB(&new_color);
    bool color_match = stroke_color && *stroke_color == new_color;
    if (cairo_pattern_get_type(fill_pattern) != CAIRO_PATTERN_TYPE_SOLID || !color_match) {
        cairo_pattern_destroy(stroke_pattern);
        stroke_pattern = cairo_pattern_create_rgba(colToDbl(new_color.r), colToDbl(new_color.g), colToDbl(new_color.b), stroke_opacity);
        stroke_color = new_color;
        LOG(printf("stroke color: %d %d %d\n", stroke_color->r, stroke_color->g, stroke_color->b));
    }
}

void CairoOutputDev::updateFillOpacity(GfxState *state)
{
    double opacity = fill_opacity;

    if (inUncoloredPattern) {
        return;
    }

    fill_opacity = state->getFillOpacity();
    if (opacity != fill_opacity) {
        if (!fill_color) {
            GfxRGB color;
            state->getFillRGB(&color);
            fill_color = color;
        }
        cairo_pattern_destroy(fill_pattern);
        fill_pattern = cairo_pattern_create_rgba(colToDbl(fill_color->r), colToDbl(fill_color->g), colToDbl(fill_color->b), fill_opacity);

        LOG(printf("fill opacity: %f\n", fill_opacity));
    }
}

void CairoOutputDev::updateStrokeOpacity(GfxState *state)
{
    double opacity = stroke_opacity;

    if (inUncoloredPattern) {
        return;
    }

    stroke_opacity = state->getStrokeOpacity();
    if (opacity != stroke_opacity) {
        if (!stroke_color) {
            GfxRGB color;
            state->getStrokeRGB(&color);
            stroke_color = color;
        }
        cairo_pattern_destroy(stroke_pattern);
        stroke_pattern = cairo_pattern_create_rgba(colToDbl(stroke_color->r), colToDbl(stroke_color->g), colToDbl(stroke_color->b), stroke_opacity);

        LOG(printf("stroke opacity: %f\n", stroke_opacity));
    }
}

void CairoOutputDev::updateFillColorStop(GfxState *state, double offset)
{
    if (inUncoloredPattern) {
        return;
    }

    GfxRGB color;
    state->getFillRGB(&color);

    // If stroke pattern is set then the current fill is clipped
    // to a stroke path.  In that case, the stroke opacity has to be used
    // rather than the fill opacity.
    // See https://gitlab.freedesktop.org/poppler/poppler/issues/178
    auto opacity = (state->getStrokePattern()) ? state->getStrokeOpacity() : state->getFillOpacity();

    cairo_pattern_add_color_stop_rgba(fill_pattern, offset, colToDbl(color.r), colToDbl(color.g), colToDbl(color.b), opacity);
    LOG(printf("fill color stop: %f (%d, %d, %d, %d)\n", offset, color.r, color.g, color.b, dblToCol(opacity)));
}

void CairoOutputDev::updateBlendMode(GfxState *state)
{
    switch (state->getBlendMode()) {
    default:
    case gfxBlendNormal:
        cairo_set_operator(cairo, CAIRO_OPERATOR_OVER);
        break;
    case gfxBlendMultiply:
        cairo_set_operator(cairo, CAIRO_OPERATOR_MULTIPLY);
        break;
    case gfxBlendScreen:
        cairo_set_operator(cairo, CAIRO_OPERATOR_SCREEN);
        break;
    case gfxBlendOverlay:
        cairo_set_operator(cairo, CAIRO_OPERATOR_OVERLAY);
        break;
    case gfxBlendDarken:
        cairo_set_operator(cairo, CAIRO_OPERATOR_DARKEN);
        break;
    case gfxBlendLighten:
        cairo_set_operator(cairo, CAIRO_OPERATOR_LIGHTEN);
        break;
    case gfxBlendColorDodge:
        cairo_set_operator(cairo, CAIRO_OPERATOR_COLOR_DODGE);
        break;
    case gfxBlendColorBurn:
        cairo_set_operator(cairo, CAIRO_OPERATOR_COLOR_BURN);
        break;
    case gfxBlendHardLight:
        cairo_set_operator(cairo, CAIRO_OPERATOR_HARD_LIGHT);
        break;
    case gfxBlendSoftLight:
        cairo_set_operator(cairo, CAIRO_OPERATOR_SOFT_LIGHT);
        break;
    case gfxBlendDifference:
        cairo_set_operator(cairo, CAIRO_OPERATOR_DIFFERENCE);
        break;
    case gfxBlendExclusion:
        cairo_set_operator(cairo, CAIRO_OPERATOR_EXCLUSION);
        break;
    case gfxBlendHue:
        cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_HUE);
        break;
    case gfxBlendSaturation:
        cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_SATURATION);
        break;
    case gfxBlendColor:
        cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_COLOR);
        break;
    case gfxBlendLuminosity:
        cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_LUMINOSITY);
        break;
    }
    LOG(printf("blend mode: %d\n", (int)state->getBlendMode()));
}

void CairoOutputDev::updateFont(GfxState *state)
{
    cairo_font_face_t *font_face;
    cairo_matrix_t matrix, invert_matrix;

    LOG(printf("updateFont() font=%s\n", state->getFont()->getName()->c_str()));

    needFontUpdate = false;

    // FIXME: use cairo font engine?
    if (textPage) {
        textPage->updateFont(state);
    }

    currentFont = fontEngine->getFont(state->getFont(), doc, printing, xref);

    if (!currentFont) {
        return;
    }

    font_face = currentFont->getFontFace();
    cairo_set_font_face(cairo, font_face);

    use_show_text_glyphs = state->getFont()->hasToUnicodeCMap() && cairo_surface_has_show_text_glyphs(cairo_get_target(cairo));

    double fontSize = state->getFontSize();
    const std::array<double, 6> &m = state->getTextMat();
    /* NOTE: adjusting by a constant is hack. The correct solution
     * is probably to use user-fonts and compute the scale on a per
     * glyph basis instead of for the entire font */
    double w = currentFont->getSubstitutionCorrection(state->getFont());
    matrix.xx = m[0] * fontSize * state->getHorizScaling() * w;
    matrix.yx = m[1] * fontSize * state->getHorizScaling() * w;
    matrix.xy = -m[2] * fontSize;
    matrix.yy = -m[3] * fontSize;
    matrix.x0 = 0;
    matrix.y0 = 0;

    LOG(printf("font matrix: %f %f %f %f\n", matrix.xx, matrix.yx, matrix.xy, matrix.yy));

    /* Make sure the font matrix is invertible before setting it.  cairo
     * will blow up if we give it a matrix that's not invertible, so we
     * need to check before passing it to cairo_set_font_matrix. Ignoring it
     * is likely to give better results than not rendering anything at
     * all. See #18254.
     */
    invert_matrix = matrix;
    if (cairo_matrix_invert(&invert_matrix)) {
        error(errSyntaxWarning, -1, "font matrix not invertible");
        text_matrix_valid = false;
        return;
    }

    cairo_set_font_matrix(cairo, &matrix);
    text_matrix_valid = true;
}

/* Tolerance in pixels for checking if strokes are horizontal or vertical
 * lines in device space */
#define STROKE_COORD_TOLERANCE 0.5

/* Align stroke coordinate i if the point is the start or end of a
 * horizontal or vertical line */
void CairoOutputDev::alignStrokeCoords(const GfxSubpath *subpath, int i, double *x, double *y)
{
    double x1, y1, x2, y2;
    bool align = false;

    x1 = subpath->getX(i);
    y1 = subpath->getY(i);
    cairo_user_to_device(cairo, &x1, &y1);

    // Does the current coord and prev coord form a horiz or vert line?
    if (i > 0 && !subpath->getCurve(i - 1)) {
        x2 = subpath->getX(i - 1);
        y2 = subpath->getY(i - 1);
        cairo_user_to_device(cairo, &x2, &y2);
        if (fabs(x2 - x1) < STROKE_COORD_TOLERANCE || fabs(y2 - y1) < STROKE_COORD_TOLERANCE) {
            align = true;
        }
    }

    // Does the current coord and next coord form a horiz or vert line?
    if (i < subpath->getNumPoints() - 1 && !subpath->getCurve(i + 1)) {
        x2 = subpath->getX(i + 1);
        y2 = subpath->getY(i + 1);
        cairo_user_to_device(cairo, &x2, &y2);
        if (fabs(x2 - x1) < STROKE_COORD_TOLERANCE || fabs(y2 - y1) < STROKE_COORD_TOLERANCE) {
            align = true;
        }
    }

    *x = subpath->getX(i);
    *y = subpath->getY(i);
    if (align) {
        /* see http://www.cairographics.org/FAQ/#sharp_lines */
        cairo_user_to_device(cairo, x, y);
        *x = floor(*x) + 0.5;
        *y = floor(*y) + 0.5;
        cairo_device_to_user(cairo, x, y);
    }
}

#undef STROKE_COORD_TOLERANCE

void CairoOutputDev::doPath(cairo_t *c, const GfxPath *path)
{
    int i, j;
    double x, y;
    cairo_new_path(c);
    for (i = 0; i < path->getNumSubpaths(); ++i) {
        const GfxSubpath *subpath = path->getSubpath(i);
        if (subpath->getNumPoints() > 0) {
            if (align_stroke_coords) {
                alignStrokeCoords(subpath, 0, &x, &y);
            } else {
                x = subpath->getX(0);
                y = subpath->getY(0);
            }
            cairo_move_to(c, x, y);
            j = 1;
            while (j < subpath->getNumPoints()) {
                if (subpath->getCurve(j)) {
                    if (align_stroke_coords) {
                        alignStrokeCoords(subpath, j + 2, &x, &y);
                    } else {
                        x = subpath->getX(j + 2);
                        y = subpath->getY(j + 2);
                    }
                    cairo_curve_to(c, subpath->getX(j), subpath->getY(j), subpath->getX(j + 1), subpath->getY(j + 1), x, y);

                    j += 3;
                } else {
                    if (align_stroke_coords) {
                        alignStrokeCoords(subpath, j, &x, &y);
                    } else {
                        x = subpath->getX(j);
                        y = subpath->getY(j);
                    }
                    cairo_line_to(c, x, y);
                    ++j;
                }
            }
            if (subpath->isClosed()) {
                LOG(printf("close\n"));
                cairo_close_path(c);
            }
        }
    }
}

void CairoOutputDev::stroke(GfxState *state)
{
    if (t3_render_state == Type3RenderMask) {
        GfxGray gray;
        state->getFillGray(&gray);
        if (colToDbl(gray) > 0.5) {
            return;
        }
    }

    if (adjusted_stroke_width) {
        align_stroke_coords = true;
    }
    doPath(cairo, state->getPath());
    align_stroke_coords = false;
    cairo_set_source(cairo, stroke_pattern);
    LOG(printf("stroke\n"));
    if (strokePathClip) {
        cairo_push_group(cairo);
        cairo_stroke(cairo);
        cairo_pop_group_to_source(cairo);
        fillToStrokePathClip();
    } else {
        cairo_stroke(cairo);
    }
    if (cairo_shape) {
        doPath(cairo_shape, state->getPath());
        cairo_stroke(cairo_shape);
    }
}

void CairoOutputDev::fill(GfxState *state)
{
    if (t3_render_state == Type3RenderMask) {
        GfxGray gray;
        state->getFillGray(&gray);
        if (colToDbl(gray) > 0.5) {
            return;
        }
    }

    doPath(cairo, state->getPath());
    cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_WINDING);
    cairo_set_source(cairo, fill_pattern);
    LOG(printf("fill\n"));
    // XXX: how do we get the path
    if (mask) {
        cairo_save(cairo);
        cairo_clip(cairo);
        if (strokePathClip) {
            cairo_push_group(cairo);
            fillToStrokePathClip();
            cairo_pop_group_to_source(cairo);
        }
        cairo_set_matrix(cairo, &mask_matrix);
        cairo_mask(cairo, mask);
        cairo_restore(cairo);
    } else if (strokePathClip) {
        fillToStrokePathClip();
    } else {
        cairo_fill(cairo);
    }
    if (cairo_shape) {
        cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_WINDING);
        doPath(cairo_shape, state->getPath());
        cairo_fill(cairo_shape);
    }
}

void CairoOutputDev::eoFill(GfxState *state)
{
    doPath(cairo, state->getPath());
    cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_EVEN_ODD);
    cairo_set_source(cairo, fill_pattern);
    LOG(printf("fill-eo\n"));

    if (mask) {
        cairo_save(cairo);
        cairo_clip(cairo);
        cairo_set_matrix(cairo, &mask_matrix);
        cairo_mask(cairo, mask);
        cairo_restore(cairo);
    } else {
        cairo_fill(cairo);
    }
    if (cairo_shape) {
        cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_EVEN_ODD);
        doPath(cairo_shape, state->getPath());
        cairo_fill(cairo_shape);
    }
}

bool CairoOutputDev::tilingPatternFill(GfxState *state, Gfx *gfxA, Catalog * /*cat*/, GfxTilingPattern *tPat, const std::array<double, 6> &mat, int /*x0*/, int /*y0*/, int /*x1*/, int /*y1*/, double xStep, double yStep)
{
    PDFRectangle box;
    Gfx *gfx;
    cairo_pattern_t *pattern;
    cairo_surface_t *surface;
    cairo_matrix_t matrix;
    cairo_matrix_t pattern_matrix;
    cairo_t *old_cairo;
    double xMin, yMin, xMax, yMax;
    double width, height;
    double scaleX, scaleY;
    int surface_width, surface_height;
    StrokePathClip *strokePathTmp;
    bool adjusted_stroke_width_tmp;
    cairo_pattern_t *maskTmp;
    const std::array<double, 4> &bbox = tPat->getBBox();
    const std::array<double, 6> &pmat = tPat->getMatrix();
    const int paintType = tPat->getPaintType();
    Dict *resDict = tPat->getResDict();
    Object *str = tPat->getContentStream();

    width = bbox[2] - bbox[0];
    height = bbox[3] - bbox[1];

    if (xStep != width || yStep != height) {
        return false;
    }
    /* TODO: implement the other cases here too */

    // Find the width and height of the transformed pattern
    cairo_get_matrix(cairo, &matrix);
    cairo_matrix_init(&pattern_matrix, mat[0], mat[1], mat[2], mat[3], mat[4], mat[5]);
    cairo_matrix_multiply(&matrix, &matrix, &pattern_matrix);

    double widthX = width, widthY = 0;
    cairo_matrix_transform_distance(&matrix, &widthX, &widthY);
    surface_width = ceil(sqrt(widthX * widthX + widthY * widthY));

    double heightX = 0, heightY = height;
    cairo_matrix_transform_distance(&matrix, &heightX, &heightY);
    surface_height = ceil(sqrt(heightX * heightX + heightY * heightY));
    scaleX = surface_width / width;
    scaleY = surface_height / height;

    surface = cairo_surface_create_similar(cairo_get_target(cairo), CAIRO_CONTENT_COLOR_ALPHA, surface_width, surface_height);
    if (cairo_surface_status(surface)) {
        return false;
    }

    old_cairo = cairo;
    cairo = cairo_create(surface);
    cairo_surface_destroy(surface);
    copyAntialias(cairo, old_cairo);

    box.x1 = bbox[0];
    box.y1 = bbox[1];
    box.x2 = bbox[2];
    box.y2 = bbox[3];
    cairo_scale(cairo, scaleX, scaleY);
    cairo_translate(cairo, -box.x1, -box.y1);

    strokePathTmp = strokePathClip;
    strokePathClip = nullptr;
    adjusted_stroke_width_tmp = adjusted_stroke_width;
    maskTmp = mask;
    mask = nullptr;
    gfx = new Gfx(doc, this, resDict, box, nullptr, nullptr, nullptr, gfxA);
    if (paintType == 2) {
        inUncoloredPattern = true;
    }
    gfx->display(str);
    if (paintType == 2) {
        inUncoloredPattern = false;
    }
    delete gfx;
    strokePathClip = strokePathTmp;
    adjusted_stroke_width = adjusted_stroke_width_tmp;
    mask = maskTmp;

    pattern = cairo_pattern_create_for_surface(cairo_get_target(cairo));
    cairo_destroy(cairo);
    cairo = old_cairo;
    if (cairo_pattern_status(pattern)) {
        return false;
    }

    // Cairo can fail if the pattern translation is too large. Fix by making the
    // translation smaller.
    const double det = pmat[0] * pmat[3] - pmat[1] * pmat[2];

    // Find the number of repetitions of pattern we need to shift by. Transform
    // the translation component of pmat (pmat[4] and pmat[5]) into the pattern's
    // coordinate system by multiplying by inverse of pmat, then divide by
    // pattern size (xStep and yStep).
    const double xoffset = round((pmat[3] * pmat[4] - pmat[2] * pmat[5]) / (xStep * det));
    const double yoffset = -round((pmat[1] * pmat[4] - pmat[0] * pmat[5]) / (yStep * det));

    if (!std::isfinite(xoffset) || !std::isfinite(yoffset)) {
        error(errSyntaxWarning, -1, "CairoOutputDev: Singular matrix in tilingPatternFill");
        return false;
    }

    // Shift pattern_matrix by multiples of the pattern size.
    pattern_matrix.x0 -= xoffset * pattern_matrix.xx * xStep + yoffset * pattern_matrix.xy * yStep;
    pattern_matrix.y0 -= xoffset * pattern_matrix.yx * xStep + yoffset * pattern_matrix.yy * yStep;

    state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
    cairo_rectangle(cairo, xMin, yMin, xMax - xMin, yMax - yMin);

    cairo_matrix_init_scale(&matrix, scaleX, scaleY);
    cairo_matrix_translate(&matrix, -box.x1, -box.y1);
    cairo_pattern_set_matrix(pattern, &matrix);

    cairo_transform(cairo, &pattern_matrix);
    cairo_set_source(cairo, pattern);
    cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT);
    if (strokePathClip) {
        fillToStrokePathClip();
    } else {
        cairo_fill(cairo);
    }

    cairo_pattern_destroy(pattern);

    return true;
}

bool CairoOutputDev::functionShadedFill(GfxState *state, GfxFunctionShading *shading)
{
    // Function shaded fills are subdivided to rectangles that are the
    // following size in device space.  Note when printing this size is
    // in points.
    const int subdivide_pixels = 10;

    // Set a minimum step to force upon {x|y}_step, to avoid approximate or reach
    // infinite loop when {x|y}_step approximates to or equals zero - Issue #1520
    const double minimum_step = 0.01;

    double x_begin, x_end, x1, x2;
    double y_begin, y_end, y1, y2;
    double x_step;
    double y_step;
    GfxColor color;
    GfxRGB rgb;
    cairo_matrix_t mat;

    const std::array<double, 6> &matrix = shading->getMatrix();
    mat.xx = matrix[0];
    mat.yx = matrix[1];
    mat.xy = matrix[2];
    mat.yy = matrix[3];
    mat.x0 = matrix[4];
    mat.y0 = matrix[5];
    if (cairo_matrix_invert(&mat)) {
        error(errSyntaxWarning, -1, "matrix not invertible");
        return false;
    }

    // get cell size in pattern space
    x_step = y_step = subdivide_pixels;
    cairo_matrix_transform_distance(&mat, &x_step, &y_step);

    if (y_step < minimum_step) {
        y_step = minimum_step;
    }
    if (x_step < minimum_step) {
        x_step = minimum_step;
    }

    cairo_pattern_destroy(fill_pattern);
    fill_pattern = cairo_pattern_create_mesh();
    cairo_pattern_set_matrix(fill_pattern, &mat);
    shading->getDomain(&x_begin, &y_begin, &x_end, &y_end);

    for (x1 = x_begin; x1 < x_end; x1 += x_step) {
        x2 = x1 + x_step;
        if (x2 > x_end) {
            x2 = x_end;
        }

        for (y1 = y_begin; y1 < y_end; y1 += y_step) {
            y2 = y1 + y_step;
            if (y2 > y_end) {
                y2 = y_end;
            }

            cairo_mesh_pattern_begin_patch(fill_pattern);
            cairo_mesh_pattern_move_to(fill_pattern, x1, y1);
            cairo_mesh_pattern_line_to(fill_pattern, x2, y1);
            cairo_mesh_pattern_line_to(fill_pattern, x2, y2);
            cairo_mesh_pattern_line_to(fill_pattern, x1, y2);

            shading->getColor(x1, y1, &color);
            shading->getColorSpace()->getRGB(color, &rgb);
            cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 0, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));

            shading->getColor(x2, y1, &color);
            shading->getColorSpace()->getRGB(color, &rgb);
            cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 1, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));

            shading->getColor(x2, y2, &color);
            shading->getColorSpace()->getRGB(color, &rgb);
            cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 2, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));

            shading->getColor(x1, y2, &color);
            shading->getColorSpace()->getRGB(color, &rgb);
            cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 3, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));

            cairo_mesh_pattern_end_patch(fill_pattern);
        }
    }

    double xMin, yMin, xMax, yMax;
    // get the clip region bbox
    state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
    state->moveTo(xMin, yMin);
    state->lineTo(xMin, yMax);
    state->lineTo(xMax, yMax);
    state->lineTo(xMax, yMin);
    state->closePath();
    fill(state);
    state->clearPath();

    return true;
}

bool CairoOutputDev::axialShadedFill(GfxState * /*state*/, GfxAxialShading *shading, double tMin, double tMax)
{
    double x0, y0, x1, y1;
    double dx, dy;

    shading->getCoords(&x0, &y0, &x1, &y1);
    dx = x1 - x0;
    dy = y1 - y0;

    cairo_pattern_destroy(fill_pattern);
    fill_pattern = cairo_pattern_create_linear(x0 + tMin * dx, y0 + tMin * dy, x0 + tMax * dx, y0 + tMax * dy);
    if (!shading->getExtend0() && !shading->getExtend1()) {
        cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_NONE);
    } else {
        cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_PAD);
    }

    LOG(printf("axial-sh\n"));

    // TODO: use the actual stops in the shading in the case
    // of linear interpolation (Type 2 Exponential functions with N=1)
    return false;
}

bool CairoOutputDev::axialShadedSupportExtend(GfxState * /*state*/, GfxAxialShading *shading)
{
    return (shading->getExtend0() == shading->getExtend1());
}

bool CairoOutputDev::radialShadedFill(GfxState * /*state*/, GfxRadialShading *shading, double sMin, double sMax)
{
    double x0, y0, r0, x1, y1, r1;
    double dx, dy, dr;
    cairo_matrix_t matrix;
    double scale;

    shading->getCoords(&x0, &y0, &r0, &x1, &y1, &r1);
    dx = x1 - x0;
    dy = y1 - y0;
    dr = r1 - r0;

    // Cairo/pixman do not work well with a very large or small scaled
    // matrix.  See cairo bug #81657.
    //
    // As a workaround, scale the pattern by the average of the vertical
    // and horizontal scaling of the current transformation matrix.
    cairo_get_matrix(cairo, &matrix);
    scale = (sqrt(matrix.xx * matrix.xx + matrix.yx * matrix.yx) + sqrt(matrix.xy * matrix.xy + matrix.yy * matrix.yy)) / 2;
    cairo_matrix_init_scale(&matrix, scale, scale);

    cairo_pattern_destroy(fill_pattern);
    fill_pattern = cairo_pattern_create_radial((x0 + sMin * dx) * scale, (y0 + sMin * dy) * scale, (r0 + sMin * dr) * scale, (x0 + sMax * dx) * scale, (y0 + sMax * dy) * scale, (r0 + sMax * dr) * scale);
    cairo_pattern_set_matrix(fill_pattern, &matrix);
    if (shading->getExtend0() && shading->getExtend1()) {
        cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_PAD);
    } else {
        cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_NONE);
    }

    LOG(printf("radial-sh\n"));

    return false;
}

bool CairoOutputDev::radialShadedSupportExtend(GfxState * /*state*/, GfxRadialShading *shading)
{
    return (shading->getExtend0() == shading->getExtend1());
}

bool CairoOutputDev::gouraudTriangleShadedFill(GfxState *state, GfxGouraudTriangleShading *shading)
{
    double x0, y0, x1, y1, x2, y2;
    GfxColor color[3];
    int i, j;
    GfxRGB rgb;

    cairo_pattern_destroy(fill_pattern);
    fill_pattern = cairo_pattern_create_mesh();

    for (i = 0; i < shading->getNTriangles(); i++) {
        if (shading->isParameterized()) {
            double color0, color1, color2;
            shading->getTriangle(i, &x0, &y0, &color0, &x1, &y1, &color1, &x2, &y2, &color2);
            shading->getParameterizedColor(color0, &color[0]);
            shading->getParameterizedColor(color1, &color[1]);
            shading->getParameterizedColor(color2, &color[2]);
        } else {
            shading->getTriangle(i, &x0, &y0, &color[0], &x1, &y1, &color[1], &x2, &y2, &color[2]);
        }

        cairo_mesh_pattern_begin_patch(fill_pattern);

        cairo_mesh_pattern_move_to(fill_pattern, x0, y0);
        cairo_mesh_pattern_line_to(fill_pattern, x1, y1);
        cairo_mesh_pattern_line_to(fill_pattern, x2, y2);

        for (j = 0; j < 3; j++) {
            shading->getColorSpace()->getRGB(color[j], &rgb);
            cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, j, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
        }

        cairo_mesh_pattern_end_patch(fill_pattern);
    }

    double xMin, yMin, xMax, yMax;
    // get the clip region bbox
    state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
    state->moveTo(xMin, yMin);
    state->lineTo(xMin, yMax);
    state->lineTo(xMax, yMax);
    state->lineTo(xMax, yMin);
    state->closePath();
    fill(state);
    state->clearPath();

    return true;
}

bool CairoOutputDev::patchMeshShadedFill(GfxState *state, GfxPatchMeshShading *shading)
{
    int i, j, k;

    cairo_pattern_destroy(fill_pattern);
    fill_pattern = cairo_pattern_create_mesh();

    for (i = 0; i < shading->getNPatches(); i++) {
        const GfxPatch *patch = shading->getPatch(i);
        GfxColor color;
        GfxRGB rgb;

        cairo_mesh_pattern_begin_patch(fill_pattern);

        cairo_mesh_pattern_move_to(fill_pattern, patch->x[0][0], patch->y[0][0]);
        cairo_mesh_pattern_curve_to(fill_pattern, patch->x[0][1], patch->y[0][1], patch->x[0][2], patch->y[0][2], patch->x[0][3], patch->y[0][3]);

        cairo_mesh_pattern_curve_to(fill_pattern, patch->x[1][3], patch->y[1][3], patch->x[2][3], patch->y[2][3], patch->x[3][3], patch->y[3][3]);

        cairo_mesh_pattern_curve_to(fill_pattern, patch->x[3][2], patch->y[3][2], patch->x[3][1], patch->y[3][1], patch->x[3][0], patch->y[3][0]);

        cairo_mesh_pattern_curve_to(fill_pattern, patch->x[2][0], patch->y[2][0], patch->x[1][0], patch->y[1][0], patch->x[0][0], patch->y[0][0]);

        cairo_mesh_pattern_set_control_point(fill_pattern, 0, patch->x[1][1], patch->y[1][1]);
        cairo_mesh_pattern_set_control_point(fill_pattern, 1, patch->x[1][2], patch->y[1][2]);
        cairo_mesh_pattern_set_control_point(fill_pattern, 2, patch->x[2][2], patch->y[2][2]);
        cairo_mesh_pattern_set_control_point(fill_pattern, 3, patch->x[2][1], patch->y[2][1]);

        for (j = 0; j < 4; j++) {
            int u, v;

            switch (j) {
            case 0:
                u = 0;
                v = 0;
                break;
            case 1:
                u = 0;
                v = 1;
                break;
            case 2:
                u = 1;
                v = 1;
                break;
            case 3:
                u = 1;
                v = 0;
                break;
            }

            if (shading->isParameterized()) {
                shading->getParameterizedColor(patch->color[u][v].c[0], &color);
            } else {
                for (k = 0; k < shading->getColorSpace()->getNComps(); k++) {
                    // simply cast to the desired type; that's all what is needed.
                    color.c[k] = static_cast<GfxColorComp>(patch->color[u][v].c[k]);
                }
            }

            shading->getColorSpace()->getRGB(color, &rgb);
            cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, j, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
        }
        cairo_mesh_pattern_end_patch(fill_pattern);
    }

    double xMin, yMin, xMax, yMax;
    // get the clip region bbox
    state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
    state->moveTo(xMin, yMin);
    state->lineTo(xMin, yMax);
    state->lineTo(xMax, yMax);
    state->lineTo(xMax, yMin);
    state->closePath();
    fill(state);
    state->clearPath();

    return true;
}

void CairoOutputDev::clip(GfxState *state)
{
    doPath(cairo, state->getPath());
    cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_WINDING);
    cairo_clip(cairo);
    LOG(printf("clip\n"));
    if (cairo_shape) {
        doPath(cairo_shape, state->getPath());
        cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_WINDING);
        cairo_clip(cairo_shape);
    }
}

void CairoOutputDev::eoClip(GfxState *state)
{
    doPath(cairo, state->getPath());
    cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_EVEN_ODD);
    cairo_clip(cairo);
    LOG(printf("clip-eo\n"));
    if (cairo_shape) {
        doPath(cairo_shape, state->getPath());
        cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_EVEN_ODD);
        cairo_clip(cairo_shape);
    }
}

void CairoOutputDev::clipToStrokePath(GfxState *state)
{
    LOG(printf("clip-to-stroke-path\n"));
    strokePathClip = static_cast<StrokePathClip *>(gmalloc(sizeof(*strokePathClip)));
    strokePathClip->path = state->getPath()->copy();
    cairo_get_matrix(cairo, &strokePathClip->ctm);
    strokePathClip->line_width = cairo_get_line_width(cairo);
    strokePathClip->dash_count = cairo_get_dash_count(cairo);
    if (strokePathClip->dash_count) {
        strokePathClip->dashes = static_cast<double *>(gmallocn(sizeof(double), strokePathClip->dash_count));
        cairo_get_dash(cairo, strokePathClip->dashes, &strokePathClip->dash_offset);
    } else {
        strokePathClip->dashes = nullptr;
    }
    strokePathClip->cap = cairo_get_line_cap(cairo);
    strokePathClip->join = cairo_get_line_join(cairo);
    strokePathClip->miter = cairo_get_miter_limit(cairo);
    strokePathClip->ref_count = 1;
}

void CairoOutputDev::fillToStrokePathClip()
{
    cairo_save(cairo);

    cairo_set_matrix(cairo, &strokePathClip->ctm);
    cairo_set_line_width(cairo, strokePathClip->line_width);
    cairo_set_dash(cairo, strokePathClip->dashes, strokePathClip->dash_count, strokePathClip->dash_offset);
    cairo_set_line_cap(cairo, strokePathClip->cap);
    cairo_set_line_join(cairo, strokePathClip->join);
    cairo_set_miter_limit(cairo, strokePathClip->miter);
    doPath(cairo, strokePathClip->path);
    cairo_stroke(cairo);

    cairo_restore(cairo);
}

void CairoOutputDev::beginString(GfxState *state, const std::string &s)
{
    int len = s.size();

    if (needFontUpdate) {
        updateFont(state);
    }

    if (!currentFont) {
        return;
    }

    glyphs = static_cast<cairo_glyph_t *>(gmallocn(len, sizeof(cairo_glyph_t)));
    glyphCount = 0;
    if (use_show_text_glyphs) {
        clusters = static_cast<cairo_text_cluster_t *>(gmallocn(len, sizeof(cairo_text_cluster_t)));
        clusterCount = 0;
        utf8Max = len * 2; // start with twice the number of glyphs. we will realloc if we need more.
        utf8 = static_cast<char *>(gmalloc(utf8Max));
        utf8Count = 0;
    }
}

void CairoOutputDev::drawChar(GfxState *state, double x, double y, double dx, double dy, double originX, double originY, CharCode code, int nBytes, const Unicode *u, int uLen)
{
    std::optional<int> glyphIndex;

    if (currentFont) {
        glyphIndex = currentFont->getGlyph(code);
        if (glyphIndex) {
            glyphs[glyphCount].index = *glyphIndex;
            glyphs[glyphCount].x = x - originX;
            glyphs[glyphCount].y = y - originY;
            glyphCount++;
        }
        if (use_show_text_glyphs) {
            const UnicodeMap *utf8Map = globalParams->getUtf8Map();
            if (utf8Max - utf8Count < uLen * 6) {
                // utf8 encoded characters can be up to 6 bytes
                if (utf8Max > uLen * 6) {
                    utf8Max *= 2;
                } else {
                    utf8Max += 2 * uLen * 6;
                }
                utf8 = static_cast<char *>(grealloc(utf8, utf8Max));
            }
            clusters[clusterCount].num_bytes = 0;
            for (int i = 0; i < uLen; i++) {
                int size = utf8Map->mapUnicode(u[i], utf8 + utf8Count, utf8Max - utf8Count);
                utf8Count += size;
                clusters[clusterCount].num_bytes += size;
            }
            clusters[clusterCount].num_glyphs = 1;
            clusterCount++;
        }
    }

    if (!textPage) {
        return;
    }
    actualText->addChar(state, x, y, dx, dy, code, nBytes, u, uLen);
}

void CairoOutputDev::endString(GfxState *state)
{
    int render;

    if (!currentFont) {
        return;
    }

    // endString can be called without a corresponding beginString. If this
    // happens glyphs will be null so don't draw anything, just return.
    // XXX: OutputDevs should probably not have to deal with this...
    if (!glyphs) {
        return;
    }

    // ignore empty strings and invisible text -- this is used by
    // Acrobat Capture
    render = state->getRender();
    if (render == 3 || glyphCount == 0 || !text_matrix_valid) {
        goto finish;
    }

    if (state->getFont()->getType() == fontType3 && render != 7) {
        // If the current font is a type 3 font, we should ignore the text rendering mode
        // (and use the default of 0) as long as we are going to either fill or stroke.
        render = 0;
    }

    if (!(render & 1)) {
        LOG(printf("fill string\n"));
        cairo_set_source(cairo, fill_pattern);
        if (use_show_text_glyphs) {
            cairo_show_text_glyphs(cairo, utf8, utf8Count, glyphs, glyphCount, clusters, clusterCount, static_cast<cairo_text_cluster_flags_t>(0));
        } else {
            cairo_show_glyphs(cairo, glyphs, glyphCount);
        }
        if (cairo_shape) {
            cairo_show_glyphs(cairo_shape, glyphs, glyphCount);
        }
    }

    // stroke
    if ((render & 3) == 1 || (render & 3) == 2) {
        LOG(printf("stroke string\n"));
        cairo_set_source(cairo, stroke_pattern);
        cairo_glyph_path(cairo, glyphs, glyphCount);
        cairo_stroke(cairo);
        if (cairo_shape) {
            cairo_glyph_path(cairo_shape, glyphs, glyphCount);
            cairo_stroke(cairo_shape);
        }
    }

    // clip
    if ((render & 4)) {
        LOG(printf("clip string\n"));
        // append the glyph path to textClipPath.

        // set textClipPath as the currentPath
        if (textClipPath) {
            cairo_append_path(cairo, textClipPath);
            if (cairo_shape) {
                cairo_append_path(cairo_shape, textClipPath);
            }
            cairo_path_destroy(textClipPath);
        }

        // append the glyph path
        cairo_glyph_path(cairo, glyphs, glyphCount);

        // move the path back into textClipPath
        // and clear the current path
        textClipPath = cairo_copy_path(cairo);
        cairo_new_path(cairo);
        if (cairo_shape) {
            cairo_new_path(cairo_shape);
        }
    }

finish:
    gfree(glyphs);
    glyphs = nullptr;
    if (use_show_text_glyphs) {
        gfree(clusters);
        clusters = nullptr;
        gfree(utf8);
        utf8 = nullptr;
    }
}

bool CairoOutputDev::beginType3Char(GfxState *state, double /*x*/, double /*y*/, double /*dx*/, double /*dy*/, CharCode /*code*/, const Unicode * /*u*/, int /*uLen*/)
{

    cairo_save(cairo);
    cairo_matrix_t matrix;

    const std::array<double, 6> &ctm = state->getCTM();
    matrix.xx = ctm[0];
    matrix.yx = ctm[1];
    matrix.xy = ctm[2];
    matrix.yy = ctm[3];
    matrix.x0 = ctm[4];
    matrix.y0 = ctm[5];
    /* Restore the original matrix and then transform to matrix needed for the
     * type3 font. This is ugly but seems to work. Perhaps there is a better way to do it?*/
    cairo_set_matrix(cairo, &orig_matrix);
    cairo_transform(cairo, &matrix);
    if (cairo_shape) {
        cairo_save(cairo_shape);
        cairo_set_matrix(cairo_shape, &orig_matrix);
        cairo_transform(cairo_shape, &matrix);
    }
    cairo_pattern_destroy(stroke_pattern);
    cairo_pattern_reference(fill_pattern);
    stroke_pattern = fill_pattern;
    return false;
}

void CairoOutputDev::endType3Char(GfxState * /*state*/)
{
    cairo_restore(cairo);
    if (cairo_shape) {
        cairo_restore(cairo_shape);
    }
}

void CairoOutputDev::type3D0(GfxState * /*state*/, double wx, double wy)
{
    t3_glyph_wx = wx;
    t3_glyph_wy = wy;
    t3_glyph_has_color = true;
}

void CairoOutputDev::type3D1(GfxState * /*state*/, double wx, double wy, double llx, double lly, double urx, double ury)
{
    t3_glyph_wx = wx;
    t3_glyph_wy = wy;
    t3_glyph_bbox[0] = llx;
    t3_glyph_bbox[1] = lly;
    t3_glyph_bbox[2] = urx;
    t3_glyph_bbox[3] = ury;
    t3_glyph_has_bbox = true;
    t3_glyph_has_color = false;
}

void CairoOutputDev::beginTextObject(GfxState * /*state*/) { }

void CairoOutputDev::endTextObject(GfxState * /*state*/)
{
    if (textClipPath) {
        // clip the accumulated text path
        cairo_append_path(cairo, textClipPath);
        cairo_clip(cairo);
        if (cairo_shape) {
            cairo_append_path(cairo_shape, textClipPath);
            cairo_clip(cairo_shape);
        }
        cairo_path_destroy(textClipPath);
        textClipPath = nullptr;
    }
}

void CairoOutputDev::beginActualText(GfxState *state, const std::string &text)
{
    if (textPage) {
        actualText->begin(state, text);
    }
}

void CairoOutputDev::endActualText(GfxState *state)
{
    if (textPage) {
        actualText->end(state);
    }
}

static inline int splashRound(double x)
{
    return static_cast<int>(floor(x + 0.5));
}

static inline int splashCeil(double x)
{
    return static_cast<int>(ceil(x));
}

static inline int splashFloor(double x)
{
    return static_cast<int>(floor(x));
}

static cairo_surface_t *cairo_surface_create_similar_clip(cairo_t *cairo, cairo_content_t content)
{
    cairo_pattern_t *pattern;
    cairo_surface_t *surface = nullptr;

    cairo_push_group_with_content(cairo, content);
    pattern = cairo_pop_group(cairo);
    cairo_pattern_get_surface(pattern, &surface);
    cairo_surface_reference(surface);
    cairo_pattern_destroy(pattern);
    return surface;
}

void CairoOutputDev::beginTransparencyGroup(GfxState * /*state*/, const std::array<double, 4> & /*bbox*/, GfxColorSpace *blendingColorSpace, bool /*isolated*/, bool knockout, bool /*forSoftMask*/)
{
    /* push color space */
    auto *css = new ColorSpaceStack;
    css->cs = blendingColorSpace;
    css->knockout = knockout;
    cairo_get_matrix(cairo, &css->group_matrix);
    css->next = groupColorSpaceStack;
    groupColorSpaceStack = css;

    LOG(printf("begin transparency group. knockout: %s\n", knockout ? "yes" : "no"));

    if (knockout) {
        knockoutCount++;
        if (!cairo_shape) {
            /* create a surface for tracking the shape */
            cairo_surface_t *cairo_shape_surface = cairo_surface_create_similar_clip(cairo, CAIRO_CONTENT_ALPHA);
            cairo_shape = cairo_create(cairo_shape_surface);
            cairo_surface_destroy(cairo_shape_surface);
            copyAntialias(cairo_shape, cairo);

            /* the color doesn't matter as long as it is opaque */
            cairo_set_source_rgb(cairo_shape, 0, 0, 0);
            cairo_matrix_t matrix;
            cairo_get_matrix(cairo, &matrix);
            cairo_set_matrix(cairo_shape, &matrix);
        }
    }
    if (groupColorSpaceStack->next && groupColorSpaceStack->next->knockout) {
        /* we need to track the shape */
        cairo_push_group(cairo_shape);
    }
    // if (false && forSoftMask) {
    //     cairo_push_group_with_content(cairo, CAIRO_CONTENT_ALPHA);
    // } else {
    cairo_push_group(cairo);
    // }

    /* push_group has an implicit cairo_save() */
    if (knockout) {
        /*XXX: let's hope this matches the semantics needed */
        cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
    } else {
        cairo_set_operator(cairo, CAIRO_OPERATOR_OVER);
    }
}

void CairoOutputDev::endTransparencyGroup(GfxState * /*state*/)
{
    if (group) {
        cairo_pattern_destroy(group);
    }
    group = cairo_pop_group(cairo);

    LOG(printf("end transparency group\n"));

    if (groupColorSpaceStack->next && groupColorSpaceStack->next->knockout) {
        if (shape) {
            cairo_pattern_destroy(shape);
        }
        shape = cairo_pop_group(cairo_shape);
    }
}

void CairoOutputDev::paintTransparencyGroup(GfxState * /*state*/, const std::array<double, 4> & /*bbox*/)
{
    LOG(printf("paint transparency group\n"));

    cairo_save(cairo);
    cairo_set_matrix(cairo, &groupColorSpaceStack->group_matrix);

    if (shape) {
        /* OPERATOR_SOURCE w/ a mask is defined as (src IN mask) ADD (dest OUT mask)
         * however our source has already been clipped to mask so we only need to
         * do ADD and OUT */

        /* clear the shape mask */
        cairo_set_source(cairo, shape);
        cairo_set_operator(cairo, CAIRO_OPERATOR_DEST_OUT);
        cairo_paint(cairo);
        cairo_set_operator(cairo, CAIRO_OPERATOR_ADD);
    }
    cairo_set_source(cairo, group);

    if (!mask) {
        cairo_paint_with_alpha(cairo, fill_opacity);
        cairo_status_t status = cairo_status(cairo);
        if (status) {
            printf("BAD status: %s\n", cairo_status_to_string(status));
        }
    } else {
        if (fill_opacity < 1.0) {
            cairo_push_group(cairo);
        }
        cairo_save(cairo);
        cairo_set_matrix(cairo, &mask_matrix);
        cairo_mask(cairo, mask);
        cairo_restore(cairo);
        if (fill_opacity < 1.0) {
            cairo_pop_group_to_source(cairo);
            cairo_paint_with_alpha(cairo, fill_opacity);
        }
        cairo_pattern_destroy(mask);
        mask = nullptr;
    }

    if (shape) {
        if (cairo_shape) {
            cairo_set_source(cairo_shape, shape);
            cairo_paint(cairo_shape);
            cairo_set_source_rgb(cairo_shape, 0, 0, 0);
        }
        cairo_pattern_destroy(shape);
        shape = nullptr;
    }

    popTransparencyGroup();
    cairo_restore(cairo);
}

static int luminocity(uint32_t x)
{
    int r = (x >> 16) & 0xff;
    int g = (x >> 8) & 0xff;
    int b = (x >> 0) & 0xff;
    // an arbitrary integer approximation of .3*r + .59*g + .11*b
    int y = (r * 19661 + g * 38666 + b * 7209 + 32829) >> 16;
    return y;
}

/* XXX: do we need to deal with shape here? */
void CairoOutputDev::setSoftMask(GfxState * /*state*/, const std::array<double, 4> & /*bbox*/, bool alpha, Function *transferFunc, const GfxColor &backdropColor)
{
    cairo_pattern_destroy(mask);

    LOG(printf("set softMask\n"));

    if (!alpha || transferFunc) {
        /* We need to mask according to the luminocity of the group.
         * So we paint the group to an image surface convert it to a luminocity map
         * and then use that as the mask. */

        /* Get clip extents in device space */
        double x1, y1, x2, y2, x_min, y_min, x_max, y_max;
        cairo_clip_extents(cairo, &x1, &y1, &x2, &y2);
        cairo_user_to_device(cairo, &x1, &y1);
        cairo_user_to_device(cairo, &x2, &y2);
        x_min = MIN(x1, x2);
        y_min = MIN(y1, y2);
        x_max = MAX(x1, x2);
        y_max = MAX(y1, y2);
        cairo_clip_extents(cairo, &x1, &y1, &x2, &y2);
        cairo_user_to_device(cairo, &x1, &y2);
        cairo_user_to_device(cairo, &x2, &y1);
        x_min = MIN(x_min, MIN(x1, x2));
        y_min = MIN(y_min, MIN(y1, y2));
        x_max = MAX(x_max, MAX(x1, x2));
        y_max = MAX(y_max, MAX(y1, y2));

        int width = static_cast<int>(ceil(x_max) - floor(x_min));
        int height = static_cast<int>(ceil(y_max) - floor(y_min));

        /* Get group device offset */
        double x_offset, y_offset;
        if (cairo_get_group_target(cairo) == cairo_get_target(cairo)) {
            cairo_surface_get_device_offset(cairo_get_group_target(cairo), &x_offset, &y_offset);
        } else {
            cairo_surface_t *pats;
            cairo_pattern_get_surface(group, &pats);
            cairo_surface_get_device_offset(pats, &x_offset, &y_offset);
        }

        /* Adjust extents by group offset */
        x_min += x_offset;
        y_min += y_offset;

        cairo_surface_t *source = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
        cairo_t *maskCtx = cairo_create(source);
        copyAntialias(maskCtx, cairo);

        // XXX: hopefully this uses the correct color space */
        if (!alpha && groupColorSpaceStack->cs) {
            GfxRGB backdropColorRGB;
            groupColorSpaceStack->cs->getRGB(backdropColor, &backdropColorRGB);
            /* paint the backdrop */
            cairo_set_source_rgb(maskCtx, colToDbl(backdropColorRGB.r), colToDbl(backdropColorRGB.g), colToDbl(backdropColorRGB.b));
        }
        cairo_paint(maskCtx);

        /* Copy source ctm to mask ctm and translate origin so that the
         * mask appears it the same location on the source surface.  */
        cairo_matrix_t mat, tmat;
        cairo_matrix_init_translate(&tmat, -x_min, -y_min);
        cairo_get_matrix(cairo, &mat);
        cairo_matrix_multiply(&mat, &mat, &tmat);
        cairo_set_matrix(maskCtx, &mat);

        /* make the device offset of the new mask match that of the group */
        cairo_surface_set_device_offset(source, x_offset, y_offset);

        /* paint the group */
        cairo_set_source(maskCtx, group);
        cairo_paint(maskCtx);

        /* XXX status = cairo_status(maskCtx); */
        cairo_destroy(maskCtx);

        /* convert to a luminocity map */
        auto *source_data = reinterpret_cast<uint32_t *>(cairo_image_surface_get_data(source));
        if (source_data) {
            /* get stride in units of 32 bits */
            ptrdiff_t stride = cairo_image_surface_get_stride(source) / 4;
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    int lum = alpha ? fill_opacity : luminocity(source_data[y * stride + x]);
                    if (transferFunc) {
                        double lum_in, lum_out;
                        lum_in = lum / 256.0;
                        transferFunc->transform(&lum_in, &lum_out);
                        lum = static_cast<int>(lum_out * 255.0 + 0.5);
                    }
                    source_data[y * stride + x] = lum << 24;
                }
            }
            cairo_surface_mark_dirty(source);
        }

        /* setup the new mask pattern */
        mask = cairo_pattern_create_for_surface(source);
        cairo_get_matrix(cairo, &mask_matrix);

        if (cairo_get_group_target(cairo) == cairo_get_target(cairo)) {
            cairo_pattern_set_matrix(mask, &mat);
        } else {
            cairo_matrix_t patMatrix;
            cairo_pattern_get_matrix(group, &patMatrix);
            /* Apply x_min, y_min offset to it appears in the same location as source. */
            cairo_matrix_multiply(&patMatrix, &patMatrix, &tmat);
            cairo_pattern_set_matrix(mask, &patMatrix);
        }

        cairo_surface_destroy(source);
    } else if (alpha) {
        mask = cairo_pattern_reference(group);
        cairo_get_matrix(cairo, &mask_matrix);
    }

    popTransparencyGroup();
}

void CairoOutputDev::popTransparencyGroup()
{
    /* pop color space */
    ColorSpaceStack *css = groupColorSpaceStack;
    if (css->knockout) {
        knockoutCount--;
        if (!knockoutCount) {
            /* we don't need to track the shape anymore because
             * we are not above any knockout groups */
            cairo_destroy(cairo_shape);
            cairo_shape = nullptr;
        }
    }
    groupColorSpaceStack = css->next;
    delete css;
}

void CairoOutputDev::clearSoftMask(GfxState * /*state*/)
{
    if (mask) {
        cairo_pattern_destroy(mask);
    }
    mask = nullptr;
}

/* Taken from cairo/doc/tutorial/src/singular.c */
static void get_singular_values(const cairo_matrix_t *matrix, double *major, double *minor)
{
    double xx = matrix->xx, xy = matrix->xy;
    double yx = matrix->yx, yy = matrix->yy;

    double a = xx * xx + yx * yx;
    double b = xy * xy + yy * yy;
    double k = xx * xy + yx * yy;

    double f = (a + b) * .5;
    double g = (a - b) * .5;
    double delta = sqrt(g * g + k * k);

    if (major) {
        *major = sqrt(f + delta);
    }
    if (minor) {
        *minor = sqrt(f - delta);
    }
}

void CairoOutputDev::getScaledSize(const cairo_matrix_t *matrix, int orig_width, int orig_height, int *scaledWidth, int *scaledHeight)
{
    double xScale;
    double yScale;
    if (orig_width > orig_height) {
        get_singular_values(matrix, &xScale, &yScale);
    } else {
        get_singular_values(matrix, &yScale, &xScale);
    }

    int tx, tx2, ty, ty2; /* the integer co-ordinates of the resulting image */
    if (xScale >= 0) {
        tx = splashRound(matrix->x0 - 0.01);
        tx2 = splashRound(matrix->x0 + xScale + 0.01) - 1;
    } else {
        tx = splashRound(matrix->x0 + 0.01) - 1;
        tx2 = splashRound(matrix->x0 + xScale - 0.01);
    }
    *scaledWidth = abs(tx2 - tx) + 1;
    // scaledWidth = splashRound(fabs(xScale));
    if (*scaledWidth == 0) {
        // technically, this should draw nothing, but it generally seems
        // better to draw a one-pixel-wide stripe rather than throwing it
        // away
        *scaledWidth = 1;
    }
    if (yScale >= 0) {
        ty = splashFloor(matrix->y0 + 0.01);
        ty2 = splashCeil(matrix->y0 + yScale - 0.01);
    } else {
        ty = splashCeil(matrix->y0 - 0.01);
        ty2 = splashFloor(matrix->y0 + yScale + 0.01);
    }
    *scaledHeight = abs(ty2 - ty);
    if (*scaledHeight == 0) {
        *scaledHeight = 1;
    }
}

cairo_filter_t CairoOutputDev::getFilterForSurface(cairo_surface_t *image, bool interpolate)
{
    if (interpolate) {
        return CAIRO_FILTER_GOOD;
    }

    int orig_width = cairo_image_surface_get_width(image);
    int orig_height = cairo_image_surface_get_height(image);
    if (orig_width == 0 || orig_height == 0) {
        return CAIRO_FILTER_NEAREST;
    }

    /* When printing, don't change the interpolation. */
    if (printing) {
        return CAIRO_FILTER_NEAREST;
    }

    cairo_matrix_t matrix;
    cairo_get_matrix(cairo, &matrix);
    int scaled_width, scaled_height;
    getScaledSize(&matrix, orig_width, orig_height, &scaled_width, &scaled_height);

    /* When scale factor is >= 400% we don't interpolate. See bugs #25268, #9860 */
    if (scaled_width / orig_width >= 4 || scaled_height / orig_height >= 4) {
        return CAIRO_FILTER_NEAREST;
    }

    return CAIRO_FILTER_GOOD;
}

void CairoOutputDev::drawImageMask(GfxState *state, Object * /*ref*/, Stream *str, int width, int height, bool invert, bool interpolate, bool /*inlineImg*/)
{

    /* FIXME: Doesn't the image mask support any colorspace? */
    cairo_set_source(cairo, fill_pattern);

    /* work around a cairo bug when scaling 1x1 surfaces */
    if (width == 1 && height == 1) {
        unsigned char pix;
        int invert_bit;

        {
            ImageStream imgStr(str, width, 1, 1);
            if (!imgStr.rewind()) {
                return;
            }
            imgStr.getPixel(&pix);
            imgStr.close();
        }

        invert_bit = invert ? 1 : 0;
        if (pix ^ invert_bit) {
            return;
        }

        cairo_save(cairo);
        cairo_rectangle(cairo, 0., 0., width, height);
        cairo_fill(cairo);
        cairo_restore(cairo);
        if (cairo_shape) {
            cairo_save(cairo_shape);
            cairo_rectangle(cairo_shape, 0., 0., width, height);
            cairo_fill(cairo_shape);
            cairo_restore(cairo_shape);
        }
        return;
    }

    /* shape is 1.0 for painted areas, 0.0 for unpainted ones */

    cairo_matrix_t matrix;
    cairo_get_matrix(cairo, &matrix);
    // XXX: it is possible that we should only do sub pixel positioning if
    // we are rendering fonts */
    drawImageMaskRegular(state, str, width, height, invert, interpolate);
}

bool CairoOutputDev::setSoftMaskFromImageMask(GfxState *state, Object * /*ref*/, Stream *str, int width, int height, bool invert, bool /*inlineImg*/, std::array<double, 6> & /*baseMatrix*/)
{

    /* FIXME: Doesn't the image mask support any colorspace? */
    cairo_set_source(cairo, fill_pattern);

    /* work around a cairo bug when scaling 1x1 surfaces */
    if (width == 1 && height == 1) {
        unsigned char pix;
        int invert_bit;

        {
            ImageStream imgStr(str, width, 1, 1);
            if (imgStr.rewind()) {
                imgStr.getPixel(&pix);
            } else {
                pix = 0;
            }
            imgStr.close();
        }

        invert_bit = invert ? 1 : 0;
        if (!(pix ^ invert_bit)) {
            cairo_save(cairo);
            cairo_rectangle(cairo, 0., 0., width, height);
            cairo_fill(cairo);
            cairo_restore(cairo);
            if (cairo_shape) {
                cairo_save(cairo_shape);
                cairo_rectangle(cairo_shape, 0., 0., width, height);
                cairo_fill(cairo_shape);
                cairo_restore(cairo_shape);
            }
        }
    } else {
        cairo_push_group_with_content(cairo, CAIRO_CONTENT_ALPHA);

        /* shape is 1.0 for painted areas, 0.0 for unpainted ones */

        cairo_matrix_t matrix;
        cairo_get_matrix(cairo, &matrix);
        // XXX: it is possible that we should only do sub pixel positioning if
        // we are rendering fonts */
        drawImageMaskRegular(state, str, width, height, invert, false);

        if (state->getFillColorSpace()->getMode() == csPattern) {
            cairo_set_source_rgb(cairo, 1, 1, 1);
            cairo_set_matrix(cairo, &mask_matrix);
            cairo_mask(cairo, mask);
        }

        if (mask) {
            cairo_pattern_destroy(mask);
        }
        mask = cairo_pop_group(cairo);
    }

    saveState(state);
    static constexpr std::array<double, 4> bbox = { 0, 0, 1, 1 }; // dummy
    beginTransparencyGroup(state, bbox, state->getFillColorSpace(), true, false, false);

    return true;
}

void CairoOutputDev::unsetSoftMaskFromImageMask(GfxState *state, std::array<double, 6> & /*baseMatrix*/)
{
    static constexpr std::array<double, 4> bbox = { 0, 0, 1, 1 }; // dummy

    endTransparencyGroup(state);
    restoreState(state);
    paintTransparencyGroup(state, bbox);
    clearSoftMask(state);
}

void CairoOutputDev::drawImageMaskRegular(GfxState *state, Stream *str, int width, int height, bool invert, bool interpolate)
{
    unsigned char *buffer;
    unsigned char *dest;
    cairo_surface_t *image;
    cairo_pattern_t *pattern;
    int x, y, i, bit;
    unsigned char *pix;
    cairo_matrix_t matrix;
    int invert_bit;
    ptrdiff_t row_stride;
    cairo_filter_t filter;

    /* TODO: Do we want to cache these? */
    ImageStream imgStr(str, width, 1, 1);
    if (!imgStr.rewind()) {
        return;
    }

    image = cairo_image_surface_create(CAIRO_FORMAT_A1, width, height);
    if (cairo_surface_status(image)) {
        goto cleanup;
    }

    buffer = cairo_image_surface_get_data(image);
    row_stride = cairo_image_surface_get_stride(image);

    invert_bit = invert ? 1 : 0;

    for (y = 0; y < height; y++) {
        pix = imgStr.getLine();
        dest = buffer + y * row_stride;
        i = 0;
        bit = 0;
        for (x = 0; x < width; x++) {
            if (bit == 0) {
                dest[i] = 0;
            }
            if (!(pix[x] ^ invert_bit)) {
#if WORDS_BIGENDIAN
                dest[i] |= (1 << (7 - bit));
#else
                dest[i] |= (1 << bit);
#endif
            }
            bit++;
            if (bit > 7) {
                bit = 0;
                i++;
            }
        }
    }

    filter = getFilterForSurface(image, interpolate);

    cairo_surface_mark_dirty(image);
    pattern = cairo_pattern_create_for_surface(image);
    cairo_surface_destroy(image);
    if (cairo_pattern_status(pattern)) {
        goto cleanup;
    }

    LOG(printf("drawImageMask %dx%d\n", width, height));

    cairo_pattern_set_filter(pattern, filter);

    cairo_matrix_init_translate(&matrix, 0, height);
    cairo_matrix_scale(&matrix, width, -height);
    cairo_pattern_set_matrix(pattern, &matrix);
    if (cairo_pattern_status(pattern)) {
        cairo_pattern_destroy(pattern);
        goto cleanup;
    }

    if (state->getFillColorSpace()->getMode() == csPattern) {
        mask = cairo_pattern_reference(pattern);
        cairo_get_matrix(cairo, &mask_matrix);
    } else if (!printing) {
        cairo_save(cairo);
        cairo_rectangle(cairo, 0., 0., 1., 1.);
        cairo_clip(cairo);
        if (strokePathClip) {
            cairo_push_group(cairo);
            fillToStrokePathClip();
            cairo_pop_group_to_source(cairo);
        }
        cairo_mask(cairo, pattern);
        cairo_restore(cairo);
    } else {
        cairo_mask(cairo, pattern);
    }

    if (cairo_shape) {
        cairo_save(cairo_shape);
        cairo_set_source(cairo_shape, pattern);
        if (!printing) {
            cairo_rectangle(cairo_shape, 0., 0., 1., 1.);
            cairo_fill(cairo_shape);
        } else {
            cairo_mask(cairo_shape, pattern);
        }
        cairo_restore(cairo_shape);
    }

    cairo_pattern_destroy(pattern);

cleanup:
    imgStr.close();
}

void CairoOutputDev::drawMaskedImage(GfxState * /*state*/, Object * /*ref*/, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, bool maskInvert,
                                     bool maskInterpolate)
{
    ptrdiff_t row_stride;
    unsigned char *maskBuffer, *buffer;
    unsigned char *maskDest;
    unsigned int *dest;
    cairo_surface_t *maskImage, *image;
    cairo_pattern_t *maskPattern, *pattern;
    cairo_matrix_t matrix;
    cairo_matrix_t maskMatrix;
    unsigned char *pix;
    int x, y;
    int invert_bit;
    cairo_filter_t filter;
    cairo_filter_t maskFilter;

    ImageStream maskImgStr(maskStr, maskWidth, 1, 1);
    if (!maskImgStr.rewind()) {
        return;
    }

    maskImage = cairo_image_surface_create(CAIRO_FORMAT_A8, maskWidth, maskHeight);
    if (cairo_surface_status(maskImage)) {
        maskImgStr.close();
        return;
    }

    maskBuffer = cairo_image_surface_get_data(maskImage);
    row_stride = cairo_image_surface_get_stride(maskImage);

    invert_bit = maskInvert ? 1 : 0;

    for (y = 0; y < maskHeight; y++) {
        pix = maskImgStr.getLine();
        maskDest = maskBuffer + y * row_stride;
        for (x = 0; x < maskWidth; x++) {
            if (pix[x] ^ invert_bit) {
                *maskDest++ = 0;
            } else {
                *maskDest++ = 255;
            }
        }
    }

    maskImgStr.close();

    maskFilter = getFilterForSurface(maskImage, maskInterpolate);

    cairo_surface_mark_dirty(maskImage);
    maskPattern = cairo_pattern_create_for_surface(maskImage);
    cairo_surface_destroy(maskImage);
    if (cairo_pattern_status(maskPattern)) {
        return;
    }

#if 0
  /* ICCBased color space doesn't do any color correction
   * so check its underlying color space as well */
  int is_identity_transform;
  is_identity_transform = colorMap->getColorSpace()->getMode() == csDeviceRGB ||
		  (colorMap->getColorSpace()->getMode() == csICCBased &&
		   ((GfxICCBasedColorSpace*)colorMap->getColorSpace())->getAlt()->getMode() == csDeviceRGB);
#endif

    /* TODO: Do we want to cache these? */
    ImageStream imgStr(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
    if (!imgStr.rewind()) {
        return;
    }

    image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
    if (cairo_surface_status(image)) {
        goto cleanup;
    }

    buffer = cairo_image_surface_get_data(image);
    row_stride = cairo_image_surface_get_stride(image);
    for (y = 0; y < height; y++) {
        dest = reinterpret_cast<unsigned int *>(buffer + y * row_stride);
        pix = imgStr.getLine();
        colorMap->getRGBLine(pix, dest, width);
    }

    filter = getFilterForSurface(image, interpolate);

    cairo_surface_mark_dirty(image);
    pattern = cairo_pattern_create_for_surface(image);
    cairo_surface_destroy(image);
    if (cairo_pattern_status(pattern)) {
        goto cleanup;
    }

    LOG(printf("drawMaskedImage %dx%d\n", width, height));

    cairo_pattern_set_filter(pattern, filter);
    cairo_pattern_set_filter(maskPattern, maskFilter);

    if (!printing) {
        cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD);
        cairo_pattern_set_extend(maskPattern, CAIRO_EXTEND_PAD);
    }

    cairo_matrix_init_translate(&matrix, 0, height);
    cairo_matrix_scale(&matrix, width, -height);
    cairo_pattern_set_matrix(pattern, &matrix);
    if (cairo_pattern_status(pattern)) {
        cairo_pattern_destroy(pattern);
        cairo_pattern_destroy(maskPattern);
        goto cleanup;
    }

    cairo_matrix_init_translate(&maskMatrix, 0, maskHeight);
    cairo_matrix_scale(&maskMatrix, maskWidth, -maskHeight);
    cairo_pattern_set_matrix(maskPattern, &maskMatrix);
    if (cairo_pattern_status(maskPattern)) {
        cairo_pattern_destroy(maskPattern);
        cairo_pattern_destroy(pattern);
        goto cleanup;
    }

    if (!printing) {
        cairo_save(cairo);
        cairo_set_source(cairo, pattern);
        cairo_rectangle(cairo, 0., 0., 1., 1.);
        cairo_clip(cairo);
        cairo_mask(cairo, maskPattern);
        cairo_restore(cairo);
    } else {
        cairo_set_source(cairo, pattern);
        cairo_mask(cairo, maskPattern);
    }

    if (cairo_shape) {
        cairo_save(cairo_shape);
        cairo_set_source(cairo_shape, pattern);
        if (!printing) {
            cairo_rectangle(cairo_shape, 0., 0., 1., 1.);
            cairo_fill(cairo_shape);
        } else {
            cairo_mask(cairo_shape, pattern);
        }
        cairo_restore(cairo_shape);
    }

    cairo_pattern_destroy(maskPattern);
    cairo_pattern_destroy(pattern);

cleanup:
    imgStr.close();
}

static inline void getMatteColorRgb(GfxImageColorMap *colorMap, const GfxColor &matteColorIn, GfxRGB *matteColorRgb)
{
    colorMap->getColorSpace()->getRGB(matteColorIn, matteColorRgb);
    matteColorRgb->r = colToByte(matteColorRgb->r);
    matteColorRgb->g = colToByte(matteColorRgb->g);
    matteColorRgb->b = colToByte(matteColorRgb->b);
}

static inline void applyMask(unsigned int *imagePointer, int length, GfxRGB matteColor, unsigned char *alphaPointer)
{
    unsigned char *p, r, g, b;
    int i;

    for (i = 0, p = reinterpret_cast<unsigned char *>(imagePointer); i < length; i++, p += 4, alphaPointer++) {
        if (*alphaPointer) {
            b = std::clamp(matteColor.b + (p[0] - matteColor.b) * 255 / *alphaPointer, 0, 255);
            g = std::clamp(matteColor.g + (p[1] - matteColor.g) * 255 / *alphaPointer, 0, 255);
            r = std::clamp(matteColor.r + (p[2] - matteColor.r) * 255 / *alphaPointer, 0, 255);
            imagePointer[i] = (r << 16) | (g << 8) | (b << 0);
        }
    }
}

// XXX: is this affect by AIS(alpha is shape)?
void CairoOutputDev::drawSoftMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, GfxImageColorMap *maskColorMap,
                                         bool maskInterpolate)
{
    ptrdiff_t row_stride, mask_row_stride;
    unsigned char *maskBuffer, *buffer;
    unsigned char *maskDest;
    unsigned int *dest;
    cairo_surface_t *maskImage, *image;
    cairo_pattern_t *maskPattern, *pattern;
    cairo_matrix_t maskMatrix, matrix;
    unsigned char *pix;
    int y;
    cairo_filter_t filter;
    cairo_filter_t maskFilter;
    GfxRGB matteColorRgb;

    // Clamp heights to what Cairo can handle - Issue #991
    if (height >= MAX_CAIRO_IMAGE_SIZE) {
        error(errInternal, -1, "Reducing image height from {0:d} to {1:d} because of Cairo limits", height, MAX_CAIRO_IMAGE_SIZE - 1);
        height = MAX_CAIRO_IMAGE_SIZE - 1;
    }

    if (maskHeight >= MAX_CAIRO_IMAGE_SIZE) {
        error(errInternal, -1, "Reducing maskImage height from {0:d} to {1:d} because of Cairo limits", maskHeight, MAX_CAIRO_IMAGE_SIZE - 1);
        maskHeight = MAX_CAIRO_IMAGE_SIZE - 1;
    }

    const GfxColor *matteColor = maskColorMap->getMatteColor();
    if (matteColor != nullptr) {
        getMatteColorRgb(colorMap, *matteColor, &matteColorRgb);
    }

    ImageStream maskImgStr(maskStr, maskWidth, maskColorMap->getNumPixelComps(), maskColorMap->getBits());
    if (!maskImgStr.rewind()) {
        return;
    }

    maskImage = cairo_image_surface_create(CAIRO_FORMAT_A8, maskWidth, maskHeight);
    if (cairo_surface_status(maskImage)) {
        maskImgStr.close();
        return;
    }

    maskBuffer = cairo_image_surface_get_data(maskImage);
    mask_row_stride = cairo_image_surface_get_stride(maskImage);
    for (y = 0; y < maskHeight; y++) {
        maskDest = (maskBuffer + y * mask_row_stride);
        pix = maskImgStr.getLine();
        if (likely(pix != nullptr)) {
            maskColorMap->getGrayLine(pix, maskDest, maskWidth);
        }
    }

    maskImgStr.close();

    maskFilter = getFilterForSurface(maskImage, maskInterpolate);

    cairo_surface_mark_dirty(maskImage);
    maskPattern = cairo_pattern_create_for_surface(maskImage);
    cairo_surface_destroy(maskImage);
    if (cairo_pattern_status(maskPattern)) {
        return;
    }

#if 0
  /* ICCBased color space doesn't do any color correction
   * so check its underlying color space as well */
  int is_identity_transform;
  is_identity_transform = colorMap->getColorSpace()->getMode() == csDeviceRGB ||
		  (colorMap->getColorSpace()->getMode() == csICCBased &&
		   ((GfxICCBasedColorSpace*)colorMap->getColorSpace())->getAlt()->getMode() == csDeviceRGB);
#endif

    /* TODO: Do we want to cache these? */
    ImageStream imgStr(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
    if (!imgStr.rewind()) {
        return;
    }

    image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
    if (cairo_surface_status(image)) {
        goto cleanup;
    }

    buffer = cairo_image_surface_get_data(image);
    row_stride = cairo_image_surface_get_stride(image);
    for (y = 0; y < height; y++) {
        dest = reinterpret_cast<unsigned int *>(buffer + y * row_stride);
        pix = imgStr.getLine();
        if (likely(pix != nullptr)) {
            colorMap->getRGBLine(pix, dest, width);
            if (matteColor != nullptr) {
                maskDest = (maskBuffer + y * mask_row_stride);
                applyMask(dest, width, matteColorRgb, maskDest);
            }
        }
    }

    filter = getFilterForSurface(image, interpolate);

    cairo_surface_mark_dirty(image);

    if (matteColor == nullptr) {
        setMimeData(state, str, ref, colorMap, image, height);
    }

    pattern = cairo_pattern_create_for_surface(image);
    cairo_surface_destroy(image);
    if (cairo_pattern_status(pattern)) {
        goto cleanup;
    }

    LOG(printf("drawSoftMaskedImage %dx%d\n", width, height));

    cairo_pattern_set_filter(pattern, filter);
    cairo_pattern_set_filter(maskPattern, maskFilter);

    if (!printing) {
        cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD);
        cairo_pattern_set_extend(maskPattern, CAIRO_EXTEND_PAD);
    }

    cairo_matrix_init_translate(&matrix, 0, height);
    cairo_matrix_scale(&matrix, width, -height);
    cairo_pattern_set_matrix(pattern, &matrix);
    if (cairo_pattern_status(pattern)) {
        cairo_pattern_destroy(pattern);
        cairo_pattern_destroy(maskPattern);
        goto cleanup;
    }

    cairo_matrix_init_translate(&maskMatrix, 0, maskHeight);
    cairo_matrix_scale(&maskMatrix, maskWidth, -maskHeight);
    cairo_pattern_set_matrix(maskPattern, &maskMatrix);
    if (cairo_pattern_status(maskPattern)) {
        cairo_pattern_destroy(maskPattern);
        cairo_pattern_destroy(pattern);
        goto cleanup;
    }

    if (fill_opacity != 1.0) {
        cairo_push_group(cairo);
    } else {
        cairo_save(cairo);
    }

    cairo_set_source(cairo, pattern);
    if (!printing) {
        cairo_rectangle(cairo, 0., 0., 1., 1.);
        cairo_clip(cairo);
    }
    cairo_mask(cairo, maskPattern);

    if (fill_opacity != 1.0) {
        cairo_pop_group_to_source(cairo);
        cairo_save(cairo);
        if (!printing) {
            cairo_rectangle(cairo, 0., 0., 1., 1.);
            cairo_clip(cairo);
        }
        cairo_paint_with_alpha(cairo, fill_opacity);
    }
    cairo_restore(cairo);

    if (cairo_shape) {
        cairo_save(cairo_shape);
        cairo_set_source(cairo_shape, pattern);
        if (!printing) {
            cairo_rectangle(cairo_shape, 0., 0., 1., 1.);
            cairo_fill(cairo_shape);
        } else {
            cairo_mask(cairo_shape, pattern);
        }
        cairo_restore(cairo_shape);
    }

    cairo_pattern_destroy(maskPattern);
    cairo_pattern_destroy(pattern);

cleanup:
    imgStr.close();
}

bool CairoOutputDev::getStreamData(Stream *str, char **buffer, int *length)
{
    int len, i;
    char *strBuffer;

    len = 0;
    str->close();
    if (!str->rewind()) {
        return false;
    }
    while (str->getChar() != EOF) {
        len++;
    }
    if (len == 0) {
        return false;
    }
    str->close();
    if (!str->rewind()) {
        return false;
    }

    strBuffer = static_cast<char *>(gmalloc(len));

    for (i = 0; i < len; ++i) {
        strBuffer[i] = str->getChar();
    }

    *buffer = strBuffer;
    *length = len;

    return true;
}

static bool colorMapHasIdentityDecodeMap(GfxImageColorMap *colorMap)
{
    for (int i = 0; i < colorMap->getNumPixelComps(); i++) {
        if (colorMap->getDecodeLow(i) != 0.0 || colorMap->getDecodeHigh(i) != 1.0) {
            return false;
        }
    }
    return true;
}

static cairo_status_t setMimeIdFromRef(cairo_surface_t *surface, const char *mime_type, const char *mime_id_prefix, Ref ref)
{
    std::string mime_id;
    char *idBuffer;
    cairo_status_t status;

    if (mime_id_prefix) {
        mime_id.append(mime_id_prefix);
    }

    GooString::appendf(mime_id, "{0:d}-{1:d}", ref.gen, ref.num);

    idBuffer = copyString(mime_id.c_str());
    status = cairo_surface_set_mime_data(surface, mime_type, reinterpret_cast<const unsigned char *>(idBuffer), mime_id.size(), gfree, idBuffer);
    if (status) {
        gfree(idBuffer);
    }
    return status;
}

bool CairoOutputDev::setMimeDataForJBIG2Globals(Stream *str, cairo_surface_t *image)
{
    auto *jb2Str = static_cast<JBIG2Stream *>(str);
    Object *globalsStr = jb2Str->getGlobalsStream();
    char *globalsBuffer;
    int globalsLength;

    // nothing to do for JBIG2 stream without Globals
    if (!globalsStr->isStream()) {
        return true;
    }

    if (setMimeIdFromRef(image, CAIRO_MIME_TYPE_JBIG2_GLOBAL_ID, nullptr, jb2Str->getGlobalsStreamRef())) {
        return false;
    }

    if (!getStreamData(globalsStr->getStream(), &globalsBuffer, &globalsLength)) {
        return false;
    }

    if (cairo_surface_set_mime_data(image, CAIRO_MIME_TYPE_JBIG2_GLOBAL, reinterpret_cast<const unsigned char *>(globalsBuffer), globalsLength, gfree, reinterpret_cast<void *>(globalsBuffer))) {
        gfree(globalsBuffer);
        return false;
    }

    return true;
}

bool CairoOutputDev::setMimeDataForCCITTParams(Stream *str, cairo_surface_t *image, int height)
{
    auto *ccittStr = static_cast<CCITTFaxStream *>(str);

    std::string params;
    GooString::appendf(params, "Columns={0:d}", ccittStr->getColumns());
    GooString::appendf(params, " Rows={0:d}", height);
    GooString::appendf(params, " K={0:d}", ccittStr->getEncoding());
    GooString::appendf(params, " EndOfLine={0:d}", ccittStr->getEndOfLine() ? 1 : 0);
    GooString::appendf(params, " EncodedByteAlign={0:d}", ccittStr->getEncodedByteAlign() ? 1 : 0);
    GooString::appendf(params, " EndOfBlock={0:d}", ccittStr->getEndOfBlock() ? 1 : 0);
    GooString::appendf(params, " BlackIs1={0:d}", ccittStr->getBlackIs1() ? 1 : 0);
    GooString::appendf(params, " DamagedRowsBeforeError={0:d}", ccittStr->getDamagedRowsBeforeError());

    char *p = strdup(params.c_str());
    if (cairo_surface_set_mime_data(image, CAIRO_MIME_TYPE_CCITT_FAX_PARAMS, reinterpret_cast<const unsigned char *>(p), params.size(), gfree, reinterpret_cast<void *>(p))) {
        gfree(p);
        return false;
    }

    return true;
}

void CairoOutputDev::setMimeData(GfxState *state, Stream *str, Object *ref, GfxImageColorMap *colorMap, cairo_surface_t *image, int height)
{
    char *strBuffer;
    int len;
    Object obj;
    StreamKind strKind = str->getKind();
    const char *mime_type;
    cairo_status_t status;

    if (!printing) {
        return;
    }

    // The cairo PS backend stores images with UNIQUE_ID in PS memory so the
    // image can be re-used multiple times. As we don't know how large the images are or
    // how many times they are used, there is no benefit in enabling this. Issue #106
    if (cairo_surface_get_type(cairo_get_target(cairo)) != CAIRO_SURFACE_TYPE_PS) {
        if (ref && ref->isRef()) {
            status = setMimeIdFromRef(image, CAIRO_MIME_TYPE_UNIQUE_ID, "poppler-surface-", ref->getRef());
            if (status) {
                return;
            }
        }
    }

    switch (strKind) {
    case strDCT:
        mime_type = CAIRO_MIME_TYPE_JPEG;
        break;
    case strJPX:
        mime_type = CAIRO_MIME_TYPE_JP2;
        break;
    case strJBIG2:
        mime_type = CAIRO_MIME_TYPE_JBIG2;
        break;
    case strCCITTFax:
        mime_type = CAIRO_MIME_TYPE_CCITT_FAX;
        break;
    default:
        mime_type = nullptr;
        break;
    }

    obj = str->getDict()->lookup("ColorSpace");
    std::unique_ptr<GfxColorSpace> colorSpace = GfxColorSpace::parse(nullptr, &obj, this, state);

    // colorspace in stream dict may be different from colorspace in jpx
    // data
    if (strKind == strJPX && colorSpace) {
        return;
    }

    // only embed mime data for gray, rgb, and cmyk colorspaces.
    if (colorSpace) {
        GfxColorSpaceMode mode = colorSpace->getMode();
        switch (mode) {
        case csDeviceGray:
        case csCalGray:
        case csDeviceRGB:
        case csDeviceRGBA:
        case csCalRGB:
        case csDeviceCMYK:
        case csICCBased:
            break;

        case csLab:
        case csIndexed:
        case csSeparation:
        case csDeviceN:
        case csPattern:
            return;
        }
    }

    if (!colorMapHasIdentityDecodeMap(colorMap)) {
        return;
    }

    if (strKind == strJBIG2 && !setMimeDataForJBIG2Globals(str, image)) {
        return;
    }

    if (strKind == strCCITTFax && !setMimeDataForCCITTParams(str, image, height)) {
        return;
    }

    if (mime_type) {
        if (getStreamData(str->getNextStream(), &strBuffer, &len)) {
            status = cairo_surface_set_mime_data(image, mime_type, reinterpret_cast<const unsigned char *>(strBuffer), len, gfree, strBuffer);
        }

        if (status) {
            gfree(strBuffer);
        }
    }
}

class RescaleDrawImage : public CairoRescaleBox
{
private:
    ImageStream *imgStr;
    GfxRGB *lookup;
    int width;
    GfxImageColorMap *colorMap;
    const int *maskColors;
    int current_row;
    bool imageError;
    bool fromRGBA;

public:
    ~RescaleDrawImage() override;
    cairo_surface_t *getSourceImage(Stream *str, int widthA, int height, int scaledWidth, int scaledHeight, bool printing, GfxImageColorMap *colorMapA, const int *maskColorsA)
    {
        cairo_surface_t *image = nullptr;
        int i;

        lookup = nullptr;
        colorMap = colorMapA;
        maskColors = maskColorsA;
        width = widthA;
        current_row = -1;
        imageError = false;
        fromRGBA = colorMap->getColorSpace()->getMode() == csDeviceRGBA;

        /* TODO: Do we want to cache these? */
        imgStr = new ImageStream(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
        if (!imgStr->rewind()) {
            delete imgStr;
            return image;
        }

#if 0
    /* ICCBased color space doesn't do any color correction
     * so check its underlying color space as well */
    int is_identity_transform;
    is_identity_transform = colorMap->getColorSpace()->getMode() == csDeviceRGB ||
      (colorMap->getColorSpace()->getMode() == csICCBased &&
       ((GfxICCBasedColorSpace*)colorMap->getColorSpace())->getAlt()->getMode() == csDeviceRGB);
#endif

        // special case for one-channel (monochrome/gray/separation) images:
        // build a lookup table here
        if (colorMap->getNumPixelComps() == 1) {
            int n;
            unsigned char pix;

            n = 1 << colorMap->getBits();
            lookup = static_cast<GfxRGB *>(gmallocn(n, sizeof(GfxRGB)));
            for (i = 0; i < n; ++i) {
                pix = static_cast<unsigned char>(i);

                colorMap->getRGB(&pix, &lookup[i]);
            }
        }

        bool needsCustomDownscaling = (width > MAX_CAIRO_IMAGE_SIZE || height > MAX_CAIRO_IMAGE_SIZE);

        if (printing) {
            if (width > MAX_PRINT_IMAGE_SIZE || height > MAX_PRINT_IMAGE_SIZE) {
                if (width > height) {
                    scaledWidth = MAX_PRINT_IMAGE_SIZE;
                    scaledHeight = MAX_PRINT_IMAGE_SIZE * static_cast<double>(height) / width;
                } else {
                    scaledHeight = MAX_PRINT_IMAGE_SIZE;
                    scaledWidth = MAX_PRINT_IMAGE_SIZE * static_cast<double>(width) / height;
                }
                needsCustomDownscaling = true;

                if (scaledWidth == 0) {
                    scaledWidth = 1;
                }
                if (scaledHeight == 0) {
                    scaledHeight = 1;
                }
            }
        }

        if (!needsCustomDownscaling || scaledWidth >= width || scaledHeight >= height) {
            // No downscaling. Create cairo image containing the source image data.
            unsigned char *buffer;
            ptrdiff_t stride;

            image = cairo_image_surface_create(maskColors || fromRGBA ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, width, height);

            if (cairo_surface_status(image)) {
                goto cleanup;
            }

            buffer = cairo_image_surface_get_data(image);
            stride = cairo_image_surface_get_stride(image);
            for (int y = 0; y < height; y++) {
                auto *dest = reinterpret_cast<uint32_t *>(buffer + y * stride);
                getRow(y, dest);
            }
        } else {
            // Downscaling required. Create cairo image the size of the
            // rescaled image and downscale the source image data into
            // the cairo image. downScaleImage() will call getRow() to read
            // source image data from the image stream. This avoids having
            // to create an image the size of the source image which may
            // exceed cairo's 32767x32767 image size limit (and also saves a
            // lot of memory).
            image = cairo_image_surface_create(maskColors || fromRGBA ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, scaledWidth, scaledHeight);
            if (cairo_surface_status(image)) {
                goto cleanup;
            }

            downScaleImage(width, height, scaledWidth, scaledHeight, 0, 0, scaledWidth, scaledHeight, image);
        }
        cairo_surface_mark_dirty(image);

    cleanup:
        gfree(lookup);
        imgStr->close();
        delete imgStr;
        return image;
    }

    void getRow(int row_num, uint32_t *row_data) override
    {
        unsigned char *pix;

        if (row_num <= current_row) {
            return;
        }

        while (current_row < row_num) {
            pix = imgStr->getLine();
            current_row++;
        }

        if (unlikely(pix == nullptr)) {
            memset(row_data, 0, width * 4);
            if (!imageError) {
                error(errInternal, -1, "Bad image stream");
                imageError = true;
            }
        } else if (lookup) {
            unsigned char *p = pix;
            GfxRGB rgb;

            for (int i = 0; i < width; i++) {
                rgb = lookup[*p];
                row_data[i] = (static_cast<int>(colToByte(rgb.r)) << 16) | (static_cast<int>(colToByte(rgb.g)) << 8) | (static_cast<int>(colToByte(rgb.b)) << 0);
                p++;
            }
        } else if (fromRGBA) {
            // Case of transparent JPX images, they contain RGBA data · Issue #1486
            auto *rgbaCS = dynamic_cast<GfxDeviceRGBAColorSpace *>(colorMap->getColorSpace());
            if (rgbaCS) {
                GfxDeviceRGBAColorSpace::getARGBPremultipliedLine(pix, row_data, width);
            } else {
                error(errSyntaxWarning, -1, "CairoOutputDev: Unexpected fallback from RGBA to RGB");
                colorMap->getRGBLine(pix, row_data, width);
            }
        } else {
            colorMap->getRGBLine(pix, row_data, width);
        }

        if (maskColors) {
            for (int x = 0; x < width; x++) {
                bool is_opaque = false;
                for (int i = 0; i < colorMap->getNumPixelComps(); ++i) {
                    if (pix[i] < maskColors[2 * i] || pix[i] > maskColors[2 * i + 1]) {
                        is_opaque = true;
                        break;
                    }
                }
                if (is_opaque) {
                    *row_data |= 0xff000000;
                } else {
                    *row_data = 0;
                }
                row_data++;
                pix += colorMap->getNumPixelComps();
            }
        }
    }
};

RescaleDrawImage::~RescaleDrawImage() = default;

void CairoOutputDev::drawImage(GfxState *state, Object *ref, Stream *str, int widthA, int heightA, GfxImageColorMap *colorMap, bool interpolate, const int *maskColors, bool inlineImg)
{
    cairo_surface_t *image;
    cairo_pattern_t *pattern, *maskPattern;
    cairo_matrix_t matrix;
    int width, height;
    int scaledWidth, scaledHeight;
    cairo_filter_t filter = CAIRO_FILTER_GOOD;
    RescaleDrawImage rescale;

    LOG(printf("drawImage %dx%d\n", widthA, heightA));

    cairo_get_matrix(cairo, &matrix);
    getScaledSize(&matrix, widthA, heightA, &scaledWidth, &scaledHeight);
    image = rescale.getSourceImage(str, widthA, heightA, scaledWidth, scaledHeight, printing, colorMap, maskColors);
    if (!image) {
        return;
    }

    width = cairo_image_surface_get_width(image);
    height = cairo_image_surface_get_height(image);
    if (width == widthA && height == heightA) {
        filter = getFilterForSurface(image, interpolate);
    }

    if (!inlineImg) { /* don't read stream twice if it is an inline image */
        setMimeData(state, str, ref, colorMap, image, heightA);
    }

    pattern = cairo_pattern_create_for_surface(image);
    cairo_surface_destroy(image);
    if (cairo_pattern_status(pattern)) {
        return;
    }

    cairo_pattern_set_filter(pattern, filter);

    if (!printing) {
        cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD);
    }

    cairo_matrix_init_translate(&matrix, 0, height);
    cairo_matrix_scale(&matrix, width, -height);
    cairo_pattern_set_matrix(pattern, &matrix);
    if (cairo_pattern_status(pattern)) {
        cairo_pattern_destroy(pattern);
        return;
    }

    if (!mask && fill_opacity != 1.0) {
        maskPattern = cairo_pattern_create_rgba(1., 1., 1., fill_opacity);
    } else if (mask) {
        maskPattern = cairo_pattern_reference(mask);
    } else {
        maskPattern = nullptr;
    }

    cairo_save(cairo);
    cairo_set_source(cairo, pattern);
    if (!printing) {
        cairo_rectangle(cairo, 0., 0., 1., 1.);
    }
    if (maskPattern) {
        if (!printing) {
            cairo_clip(cairo);
        }
        if (mask) {
            cairo_set_matrix(cairo, &mask_matrix);
        }
        cairo_mask(cairo, maskPattern);
    } else {
        if (printing) {
            cairo_paint(cairo);
        } else {
            cairo_fill(cairo);
        }
    }
    cairo_restore(cairo);

    cairo_pattern_destroy(maskPattern);

    if (cairo_shape) {
        cairo_save(cairo_shape);
        cairo_set_source(cairo_shape, pattern);
        if (printing) {
            cairo_paint(cairo_shape);
        } else {
            cairo_rectangle(cairo_shape, 0., 0., 1., 1.);
            cairo_fill(cairo_shape);
        }
        cairo_restore(cairo_shape);
    }

    cairo_pattern_destroy(pattern);
}

void CairoOutputDev::beginMarkedContent(const std::string &name, Dict *properties)
{
    if (!logicalStruct || !isPDF()) {
        return;
    }

    if (name == "Artifact") {
        markedContentStack.emplace_back(name);
        cairo_tag_begin(cairo, name.c_str(), nullptr);
        return;
    }

    int mcid = -1;
    if (properties) {
        properties->lookupInt("MCID", {}, &mcid);
    }

    if (mcid == -1) {
        return;
    }

    std::string attribs = GooString::format("tag_name='{0:r}' id='{1:d}_{2:d}'", &name, currentStructParents, mcid);
    mcidEmitted.insert(std::pair<int, int>(currentStructParents, mcid));

    std::string tag;
    tag = CAIRO_TAG_CONTENT;
    cairo_tag_begin(cairo, CAIRO_TAG_CONTENT, attribs.c_str());

    markedContentStack.push_back(tag);
}

void CairoOutputDev::endMarkedContent(GfxState * /*state*/)
{
    if (!logicalStruct || !isPDF()) {
        return;
    }

    if (markedContentStack.empty()) {
        return;
    }

    cairo_tag_end(cairo, markedContentStack.back().c_str());
    markedContentStack.pop_back();
}

//------------------------------------------------------------------------
// ImageOutputDev
//------------------------------------------------------------------------

CairoImageOutputDev::CairoImageOutputDev()
{
    images = nullptr;
    numImages = 0;
    size = 0;
    imgDrawCbk = nullptr;
    imgDrawCbkData = nullptr;
}

CairoImageOutputDev::~CairoImageOutputDev()
{
    int i;

    for (i = 0; i < numImages; i++) {
        delete images[i];
    }
    gfree(static_cast<void *>(images));
}

void CairoImageOutputDev::saveImage(CairoImage *image)
{
    if (numImages >= size) {
        size += 16;
        images = static_cast<CairoImage **>(greallocn(static_cast<void *>(images), size, sizeof(CairoImage *)));
    }
    images[numImages++] = image;
}

void CairoImageOutputDev::getBBox(GfxState *state, int width, int height, double *x1, double *y1, double *x2, double *y2)
{
    const std::array<double, 6> &ctm = state->getCTM();
    cairo_matrix_t matrix;
    cairo_matrix_init(&matrix, ctm[0], ctm[1], -ctm[2], -ctm[3], ctm[2] + ctm[4], ctm[3] + ctm[5]);

    int scaledWidth, scaledHeight;
    getScaledSize(&matrix, width, height, &scaledWidth, &scaledHeight);

    if (matrix.xx >= 0) {
        *x1 = matrix.x0;
    } else {
        *x1 = matrix.x0 - scaledWidth;
    }
    *x2 = *x1 + scaledWidth;

    if (matrix.yy >= 0) {
        *y1 = matrix.y0;
    } else {
        *y1 = matrix.y0 - scaledHeight;
    }
    *y2 = *y1 + scaledHeight;
}

void CairoImageOutputDev::drawImageMask(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool interpolate, bool inlineImg)
{
    cairo_t *cr;
    cairo_surface_t *surface;
    double x1, y1, x2, y2;
    CairoImage *image;

    getBBox(state, width, height, &x1, &y1, &x2, &y2);

    image = new CairoImage(x1, y1, x2, y2);
    saveImage(image);

    if (imgDrawCbk && imgDrawCbk(numImages - 1, imgDrawCbkData)) {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
        cr = cairo_create(surface);
        setCairo(cr);
        cairo_translate(cr, 0, height);
        cairo_scale(cr, width, -height);

        CairoOutputDev::drawImageMask(state, ref, str, width, height, invert, interpolate, inlineImg);
        image->setImage(surface);

        setCairo(nullptr);
        cairo_surface_destroy(surface);
        cairo_destroy(cr);
    }
}

bool CairoImageOutputDev::setSoftMaskFromImageMask(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool inlineImg, std::array<double, 6> & /*baseMatrix*/)
{
    cairo_t *cr;
    cairo_surface_t *surface;
    double x1, y1, x2, y2;
    CairoImage *image;

    getBBox(state, width, height, &x1, &y1, &x2, &y2);

    image = new CairoImage(x1, y1, x2, y2);
    saveImage(image);

    if (imgDrawCbk && imgDrawCbk(numImages - 1, imgDrawCbkData)) {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
        cr = cairo_create(surface);
        setCairo(cr);
        cairo_translate(cr, 0, height);
        cairo_scale(cr, width, -height);

        CairoOutputDev::drawImageMask(state, ref, str, width, height, invert, inlineImg, false);
        if (state->getFillColorSpace()->getMode() == csPattern) {
            cairo_mask(cairo, mask);
        }
        image->setImage(surface);

        setCairo(nullptr);
        cairo_surface_destroy(surface);
        cairo_destroy(cr);
    }

    return true;
}

void CairoImageOutputDev::drawImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, const int *maskColors, bool inlineImg)
{
    cairo_t *cr;
    cairo_surface_t *surface;
    double x1, y1, x2, y2;
    CairoImage *image;

    getBBox(state, width, height, &x1, &y1, &x2, &y2);

    image = new CairoImage(x1, y1, x2, y2);
    saveImage(image);

    if (imgDrawCbk && imgDrawCbk(numImages - 1, imgDrawCbkData)) {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
        cr = cairo_create(surface);
        setCairo(cr);
        cairo_translate(cr, 0, height);
        cairo_scale(cr, width, -height);

        CairoOutputDev::drawImage(state, ref, str, width, height, colorMap, interpolate, maskColors, inlineImg);
        image->setImage(surface);

        setCairo(nullptr);
        cairo_surface_destroy(surface);
        cairo_destroy(cr);
    }
}

void CairoImageOutputDev::drawSoftMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, GfxImageColorMap *maskColorMap,
                                              bool maskInterpolate)
{
    cairo_t *cr;
    cairo_surface_t *surface;
    double x1, y1, x2, y2;
    CairoImage *image;

    getBBox(state, width, height, &x1, &y1, &x2, &y2);

    image = new CairoImage(x1, y1, x2, y2);
    saveImage(image);

    if (imgDrawCbk && imgDrawCbk(numImages - 1, imgDrawCbkData)) {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
        cr = cairo_create(surface);
        setCairo(cr);
        cairo_translate(cr, 0, height);
        cairo_scale(cr, width, -height);

        CairoOutputDev::drawSoftMaskedImage(state, ref, str, width, height, colorMap, interpolate, maskStr, maskWidth, maskHeight, maskColorMap, maskInterpolate);
        image->setImage(surface);

        setCairo(nullptr);
        cairo_surface_destroy(surface);
        cairo_destroy(cr);
    }
}

void CairoImageOutputDev::drawMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, bool maskInvert, bool maskInterpolate)
{
    cairo_t *cr;
    cairo_surface_t *surface;
    double x1, y1, x2, y2;
    CairoImage *image;

    getBBox(state, width, height, &x1, &y1, &x2, &y2);

    image = new CairoImage(x1, y1, x2, y2);
    saveImage(image);

    if (imgDrawCbk && imgDrawCbk(numImages - 1, imgDrawCbkData)) {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
        cr = cairo_create(surface);
        setCairo(cr);
        cairo_translate(cr, 0, height);
        cairo_scale(cr, width, -height);

        CairoOutputDev::drawMaskedImage(state, ref, str, width, height, colorMap, interpolate, maskStr, maskWidth, maskHeight, maskInvert, maskInterpolate);
        image->setImage(surface);

        setCairo(nullptr);
        cairo_surface_destroy(surface);
        cairo_destroy(cr);
    }
}
