// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Native PDF import using libpoppler.
 *
 * Authors:
 *   miklos erdelyi
 *   Jon A. Cruz <jon@joncruz.org>
 *
 * Copyright (C) 2007 Authors
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 *
 */

#ifdef HAVE_CONFIG_H
# include "config.h"  // only include where actually required!
#endif

#include <string>
#include <locale>
#include <codecvt>

#ifdef HAVE_POPPLER
#define USE_CMS

#include "Function.h"
#include "GfxFont.h"
#include "GfxState.h"
#include "GlobalParams.h"
#include "Page.h"
#include "Stream.h"
#include "UnicodeMap.h"
#include "color.h"
#include "display/cairo-utils.h"
#include "display/nr-filter-utils.h"
#include "document.h"
#include "extract-uri.h"
#include "libnrtype/font-factory.h"
#include "libnrtype/font-instance.h"
#include "object/color-profile.h"
#include "object/sp-defs.h"
#include "object/sp-item-group.h"
#include "object/sp-namedview.h"
#include "pdf-parser.h"
#include "pdf-utils.h"
#include "png.h"
#include "poppler-cairo-font-engine.h"
#include "profile-manager.h"
#include "svg-builder.h"
#include "svg/css-ostringstream.h"
#include "svg/path-string.h"
#include "svg/svg-color.h"
#include "svg/svg.h"
#include "util/units.h"
#include "xml/document.h"
#include "xml/node.h"
#include "xml/repr.h"
#include "xml/sp-css-attr.h"

namespace Inkscape {
namespace Extension {
namespace Internal {

//#define IFTRACE(_code)  _code
#define IFTRACE(_code)

#define TRACE(_args) IFTRACE(g_print _args)


/**
 * \class SvgBuilder
 *
 */

SvgBuilder::SvgBuilder(SPDocument *document, gchar *docname, XRef *xref)
{
    _is_top_level = true;
    _doc = document;
    _docname = docname;
    _xref = xref;
    _xml_doc = _doc->getReprDoc();
    _container = _root = _doc->getReprRoot();
    _init();

    // Set default preference settings
    _preferences = _xml_doc->createElement("svgbuilder:prefs");
    _preferences->setAttribute("embedImages", "1");
}

SvgBuilder::SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root) {
    _is_top_level = false;
    _doc = parent->_doc;
    _docname = parent->_docname;
    _xref = parent->_xref;
    _xml_doc = parent->_xml_doc;
    _preferences = parent->_preferences;
    _container = this->_root = root;
    _init();
}

SvgBuilder::~SvgBuilder()
{
    if (_clip_history) {
        delete _clip_history;
        _clip_history = nullptr;
    }
}

void SvgBuilder::_init() {
    _clip_history = new ClipHistoryEntry();
    _css_font = nullptr;
    _font_specification = nullptr;
    _in_text_object = false;
    _invalidated_style = true;
    _width = 0;
    _height = 0;

    _node_stack.push_back(_container);
}

/**
 * We're creating a multi-page document, push page number.
 */
void SvgBuilder::pushPage(const std::string &label, GfxState *state)
{
    // Move page over by the last page width
    if (_page && this->_width) {
        int gap = 20;
        _page_left += this->_width + gap;
        // TODO: A more interesting page layout could be implemented here.
    }
    _page_num += 1;
    _page_offset = true;

    if (_page) {
        Inkscape::GC::release(_page);
    }
    _page = _xml_doc->createElement("inkscape:page");
    _page->setAttributeSvgDouble("x", _page_left);
    _page->setAttributeSvgDouble("y", _page_top);

    // Page translation is somehow lost in the way we're using poppler and the state management
    // Applying the state directly doesn't work as many of the flips/rotates are baked in already.
    // The translation alone must be added back to the page position so items end up in the
    // right places. If a better method is found, please replace this code.
    auto st = stateToAffine(state);
    auto tr = st.translation();
    if (st[0] < 0 || st[2] < 0) { // Flip or rotate in X
        tr[Geom::X] = -tr[Geom::X] + state->getPageWidth();
    }
    if (st[1] < 0 || st[3] < 0) { // Flip or rotate in Y
        tr[Geom::Y] = -tr[Geom::Y] + state->getPageHeight();
    }
    // Note: This translation is very rare in pdf files, most of the time their initial state doesn't contain
    // any real translations, just a flip and the because of our GfxState constructor, the pt/px scale.
    // Please use an example pdf which produces a non-zero translation in order to change this code!
    _page_affine = Geom::Translate(tr).inverse() * Geom::Translate(_page_left, _page_top);

    if (!label.empty()) {
        _page->setAttribute("inkscape:label", label);
    }
    auto _nv = _doc->getNamedView()->getRepr();
    _nv->appendChild(_page);

    // No OptionalContentGroups means no layers, so make a default layer for this page.
    if (_ocgs.empty()) {
        // Reset to root
        while (_container != _root) {
            _popGroup();
        }
        _pushGroup();
        setAsLayer(label.c_str(), true);
    }
}

void SvgBuilder::setDocumentSize(double width, double height) {
    this->_width = width;
    this->_height = height;

    if (_page_num < 2) {
        _root->setAttributeSvgDouble("width", width);
        _root->setAttributeSvgDouble("height", height);
    }
    if (_page) {
        _page->setAttributeSvgDouble("width", width);
        _page->setAttributeSvgDouble("height", height);
    }
}

/**
 * Crop to this bounding box, do this before setMargins() but after setDocumentSize
 */
void SvgBuilder::cropPage(const Geom::Rect &bbox)
{
    if (_container == _root) {
        // We're not going to crop when there's PDF Layers
        return;
    }
    auto box = bbox * _page_affine;
    Inkscape::CSSOStringStream val;
    val << "M" << box.left() << " " << box.top()
        << "H" << box.right() << "V" << box.bottom()
        << "H" << box.left() << "Z";
    auto clip_path = _createClip(val.str(), Geom::identity(), false);
    gchar *urltext = g_strdup_printf("url(#%s)", clip_path->attribute("id"));
    _container->setAttribute("clip-path", urltext);
    g_free(urltext);
}

/**
 * Calculate the page margin size based on the pdf settings.
 */
void SvgBuilder::setMargins(const Geom::Rect &page, const Geom::Rect &margins, const Geom::Rect &bleed)
{
    if (page.width() != _width || page.height() != _height) {
        // We need to re-set the page size and change the page_affine.
        _page_affine *= Geom::Translate(-page.left(), -page.top());
        setDocumentSize(page.width(), page.height());
    }
    if (page != margins) {
        if (!_page) {
            g_warning("Can not store PDF margins in bare document.");
            return;
        }
        // Calculate the margins from the pdf art box.
        Inkscape::CSSOStringStream val;
        val << margins.top() - page.top() << " "
            << page.right() - margins.right() << " "
            << page.bottom() - margins.bottom() << " "
            << margins.left() - page.left();
        _page->setAttribute("margin", val.str());
    }
    if (page != bleed) {
        if (!_page) {
            g_warning("Can not store PDF bleed in bare document.");
            return;
        }
        Inkscape::CSSOStringStream val;
        val << page.top() - bleed.top() << " "
            << bleed.right() - page.right() << " "
            << bleed.bottom() - page.bottom() << " "
            << page.left() - bleed.left();
        _page->setAttribute("bleed", val.str());
    }
}

/**
 * \brief Sets groupmode of the current container to 'layer' and sets its label if given
 */
void SvgBuilder::setAsLayer(const char *layer_name, bool visible)
{
    _container->setAttribute("inkscape:groupmode", "layer");
    if (layer_name) {
        _container->setAttribute("inkscape:label", layer_name);
    }
    if (!visible) {
        SPCSSAttr *css = sp_repr_css_attr_new();
        sp_repr_css_set_property(css, "display", "none");
        sp_repr_css_change(_container, css, "style");
    }
}

/**
 * \brief Sets the current container's opacity
 */
void SvgBuilder::setGroupOpacity(double opacity) {
    _container->setAttributeSvgDouble("opacity", CLAMP(opacity, 0.0, 1.0));
}

void SvgBuilder::saveState(GfxState *state)
{
    _clip_history = _clip_history->save();
}

void SvgBuilder::restoreState(GfxState *state) {
    _clip_history = _clip_history->restore();

    if (!_mask_groups.empty()) {
        GfxState *mask_state = _mask_groups.back();
        if (state == mask_state) {
            popGroup(state);
            _mask_groups.pop_back();
        }
    }
    while (_clip_groups > 0) {
        popGroup(nullptr);
        _clip_groups--;
    }
}

Inkscape::XML::Node *SvgBuilder::_pushContainer(const char *name)
{
    return _pushContainer(_xml_doc->createElement(name));
}

Inkscape::XML::Node *SvgBuilder::_pushContainer(Inkscape::XML::Node *node)
{
    _node_stack.push_back(node);
    _container = node;
    // Clear the clip history
    _clip_history = _clip_history->save(true);
    return node;
}

Inkscape::XML::Node *SvgBuilder::_popContainer()
{
    Inkscape::XML::Node *node = nullptr;
    if ( _node_stack.size() > 1 ) {
        node = _node_stack.back();
        _node_stack.pop_back();
        _container = _node_stack.back();    // Re-set container
        _clip_history = _clip_history->restore();
    } else {
        TRACE(("_popContainer() called when stack is empty\n"));
        node = _root;
    }
    return node;
}

/**
 * Create an svg element and append it to the current container object.
 */
Inkscape::XML::Node *SvgBuilder::_addToContainer(const char *name)
{
    Inkscape::XML::Node *node = _xml_doc->createElement(name);
    _addToContainer(node);
    return node;
}

/**
 * Append the given xml element to the current container object, clipping and masking as needed.
 *
 * if release is true (default), the XML node will be GC released too.
 */
void SvgBuilder::_addToContainer(Inkscape::XML::Node *node, bool release)
{
    if (!node->parent()) {
        _container->appendChild(node);
    }
    if (release) {
        Inkscape::GC::release(node);
    }
}

void SvgBuilder::_setClipPath(Inkscape::XML::Node *node)
{
    if (_clip_history->hasClipPath() || _clip_text) {
        auto tr = Geom::identity();
        if (auto attr = node->attribute("transform")) {
            sp_svg_transform_read(attr, &tr);
        }
        if (auto clip_path = _getClip(tr)) {
            gchar *urltext = g_strdup_printf("url(#%s)", clip_path->attribute("id"));
            node->setAttribute("clip-path", urltext);
            g_free(urltext);
        }
    }
}

Inkscape::XML::Node *SvgBuilder::_pushGroup()
{
    Inkscape::XML::Node *saved_container = _container;
    Inkscape::XML::Node *node = _pushContainer("svg:g");
    saved_container->appendChild(node);
    Inkscape::GC::release(node);
    return _container;
}

Inkscape::XML::Node *SvgBuilder::_popGroup()
{
    if (_container != _root) { // Pop if the current container isn't root
        _popContainer();
    }
    return _container;
}

static gchar *svgConvertRGBToText(double r, double g, double b) {
    using Inkscape::Filters::clamp;
    static gchar tmp[1023] = {0};
    snprintf(tmp, 1023,
             "#%02x%02x%02x",
             clamp(SP_COLOR_F_TO_U(r)),
             clamp(SP_COLOR_F_TO_U(g)),
             clamp(SP_COLOR_F_TO_U(b)));
    return (gchar *)&tmp;
}

static std::string svgConvertGfxRGB(GfxRGB *color)
{
    double r = (double)color->r / 65535.0;
    double g = (double)color->g / 65535.0;
    double b = (double)color->b / 65535.0;
    return svgConvertRGBToText(r, g, b);
}

std::string SvgBuilder::convertGfxColor(const GfxColor *color, GfxColorSpace *space)
{
    std::string icc = "";
    switch (space->getMode()) {
        case csDeviceGray:
        case csDeviceRGB:
        case csDeviceCMYK:
            icc = _icc_profile;
            break;
        case csICCBased:
#if POPPLER_CHECK_VERSION(0, 90, 0)
            auto icc_space = dynamic_cast<GfxICCBasedColorSpace *>(space);
            icc = _getColorProfile(icc_space->getProfile().get());
#else
            g_warning("ICC profile ignored; libpoppler >= 0.90.0 required.");
#endif
            break;
    }

    GfxRGB rgb;
    space->getRGB(color, &rgb);
    auto rgb_color = svgConvertGfxRGB(&rgb);

    if (!icc.empty()) {
        Inkscape::CSSOStringStream icc_color;
        icc_color << rgb_color << " icc-color(" << icc;
        for (int i = 0; i < space->getNComps(); ++i) {
            icc_color << ", " << colToDbl((*color).c[i]);
        }
        icc_color << ");";
        return icc_color.str();
    }

    return rgb_color;
}

static void svgSetTransform(Inkscape::XML::Node *node, Geom::Affine matrix) {
    if (node->attribute("clip-path")) {
        g_error("Adding transform AFTER clipping path.");
    }
    node->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(matrix));
}

/**
 * \brief Generates a SVG path string from poppler's data structure
 */
static gchar *svgInterpretPath(_POPPLER_CONST_83 GfxPath *path) {
    Inkscape::SVG::PathString pathString;
    for (int i = 0 ; i < path->getNumSubpaths() ; ++i ) {
        _POPPLER_CONST_83 GfxSubpath *subpath = path->getSubpath(i);
        if (subpath->getNumPoints() > 0) {
            pathString.moveTo(subpath->getX(0), subpath->getY(0));
            int j = 1;
            while (j < subpath->getNumPoints()) {
                if (subpath->getCurve(j)) {
                    pathString.curveTo(subpath->getX(j), subpath->getY(j),
                                       subpath->getX(j+1), subpath->getY(j+1),
                                       subpath->getX(j+2), subpath->getY(j+2));

                    j += 3;
                } else {
                    pathString.lineTo(subpath->getX(j), subpath->getY(j));
                    ++j;
                }
            }
            if (subpath->isClosed()) {
                pathString.closePath();
            }
        }
    }

    return g_strdup(pathString.c_str());
}

/**
 * \brief Sets stroke style from poppler's GfxState data structure
 * Uses the given SPCSSAttr for storing the style properties
 */
void SvgBuilder::_setStrokeStyle(SPCSSAttr *css, GfxState *state) {
    // Stroke color/pattern
    auto space = state->getStrokeColorSpace();
    if (space->getMode() == csPattern) {
        gchar *urltext = _createPattern(state->getStrokePattern(), state, true);
        sp_repr_css_set_property(css, "stroke", urltext);
        if (urltext) {
            g_free(urltext);
        }
    } else {
        sp_repr_css_set_property(css, "stroke", convertGfxColor(state->getStrokeColor(), space).c_str());
    }

    // Opacity
    Inkscape::CSSOStringStream os_opacity;
    os_opacity << state->getStrokeOpacity();
    sp_repr_css_set_property(css, "stroke-opacity", os_opacity.str().c_str());

    // Line width
    Inkscape::CSSOStringStream os_width;
    double lw = state->getLineWidth();
    // emit a stroke which is 1px in toplevel user units
    os_width << (lw > 0.0 ? lw : 1.0);
    sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str());

    // Line cap
    switch (state->getLineCap()) {
        case 0:
            sp_repr_css_set_property(css, "stroke-linecap", "butt");
            break;
        case 1:
            sp_repr_css_set_property(css, "stroke-linecap", "round");
            break;
        case 2:
            sp_repr_css_set_property(css, "stroke-linecap", "square");
            break;
    }

    // Line join
    switch (state->getLineJoin()) {
        case 0:
            sp_repr_css_set_property(css, "stroke-linejoin", "miter");
            break;
        case 1:
            sp_repr_css_set_property(css, "stroke-linejoin", "round");
            break;
        case 2:
            sp_repr_css_set_property(css, "stroke-linejoin", "bevel");
            break;
    }

    // Miterlimit
    Inkscape::CSSOStringStream os_ml;
    os_ml << state->getMiterLimit();
    sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str());

    // Line dash
    int dash_length;
    double dash_start;
#if POPPLER_CHECK_VERSION(22, 9, 0)
    const double *dash_pattern;
    const std::vector<double> &dash = state->getLineDash(&dash_start);
    dash_pattern = dash.data();
    dash_length = dash.size();
#else
    double *dash_pattern;
    state->getLineDash(&dash_pattern, &dash_length, &dash_start);
#endif
    if ( dash_length > 0 ) {
        Inkscape::CSSOStringStream os_array;
        for ( int i = 0 ; i < dash_length ; i++ ) {
            os_array << dash_pattern[i];
            if (i < (dash_length - 1)) {
                os_array << ",";
            }
        }
        sp_repr_css_set_property(css, "stroke-dasharray", os_array.str().c_str());

        Inkscape::CSSOStringStream os_offset;
        os_offset << dash_start;
        sp_repr_css_set_property(css, "stroke-dashoffset", os_offset.str().c_str());
    } else {
        sp_repr_css_set_property(css, "stroke-dasharray", "none");
        sp_repr_css_set_property(css, "stroke-dashoffset", nullptr);
    }
}

/**
 * \brief Sets fill style from poppler's GfxState data structure
 * Uses the given SPCSSAttr for storing the style properties.
 */
void SvgBuilder::_setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd) {

    // Fill color/pattern
    auto space = state->getFillColorSpace();
    if (space->getMode() == csPattern) {
        gchar *urltext = _createPattern(state->getFillPattern(), state);
        sp_repr_css_set_property(css, "fill", urltext);
        if (urltext) {
            g_free(urltext);
        }
    } else {
        sp_repr_css_set_property(css, "fill", convertGfxColor(state->getFillColor(), space).c_str());
    }

    // Opacity
    Inkscape::CSSOStringStream os_opacity;
    os_opacity << state->getFillOpacity();
    sp_repr_css_set_property(css, "fill-opacity", os_opacity.str().c_str());

    // Fill rule
    sp_repr_css_set_property(css, "fill-rule", even_odd ? "evenodd" : "nonzero");
}

/**
 * \brief Sets blend style properties from poppler's GfxState data structure
 * \update a SPCSSAttr with all mix-blend-mode set
 */
void SvgBuilder::_setBlendMode(Inkscape::XML::Node *node, GfxState *state)
{
    SPCSSAttr *css = sp_repr_css_attr(node, "style");
    GfxBlendMode blendmode = state->getBlendMode();
    if (blendmode) {
        sp_repr_css_set_property(css, "mix-blend-mode", enum_blend_mode[blendmode].key);
    }
    Glib::ustring value;
    sp_repr_css_write_string(css, value);
    node->setAttributeOrRemoveIfEmpty("style", value);
    sp_repr_css_attr_unref(css);
}

void SvgBuilder::_setTransform(Inkscape::XML::Node *node, GfxState *state, Geom::Affine extra)
{
    svgSetTransform(node, extra * stateToAffine(state) * _page_affine);
}

/**
 * \brief Sets style properties from poppler's GfxState data structure
 * \return SPCSSAttr with all the relevant properties set
 */
SPCSSAttr *SvgBuilder::_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd) {
    SPCSSAttr *css = sp_repr_css_attr_new();
    if (fill) {
        _setFillStyle(css, state, even_odd);
    } else {
        sp_repr_css_set_property(css, "fill", "none");
    }

    if (stroke) {
        _setStrokeStyle(css, state);
    } else {
        sp_repr_css_set_property(css, "stroke", "none");
    }

    return css;
}

/**
 * Returns the CSSAttr of the previously added path if it's exactly
 * the same path AND is missing the fill or stroke that is now being painted.
 */
bool SvgBuilder::shouldMergePath(bool is_fill, const std::string &path)
{
    auto prev = _container->lastChild();
    if (!prev || prev->attribute("mask"))
        return false;

    auto prev_d = prev->attribute("d");
    if (!prev_d)
        return false;

    if (path != prev_d && path != std::string(prev_d) + " Z")
        return false;

    auto prev_css = sp_repr_css_attr(prev, "style");
    std::string prev_val = sp_repr_css_property(prev_css, is_fill ? "fill" : "stroke", "");
    // Very specific check excludes paths created elsewhere who's fill/stroke was unset.
    return prev_val == "none";
}

/**
 * Set the fill XOR stroke of the previously added path, if that path
 * is missing the given attribute AND the path is exactly the same.
 *
 * This effectively merges the two objects and is an 'interpretation' step.
 */
bool SvgBuilder::mergePath(GfxState *state, bool is_fill, const std::string &path, bool even_odd)
{
    if (shouldMergePath(is_fill, path)) {
        auto prev = _container->lastChild();
        SPCSSAttr *css = sp_repr_css_attr_new();
        if (is_fill) {
            _setFillStyle(css, state, even_odd);
            // Fill after stroke indicates a different paint order.
            sp_repr_css_set_property(css, "paint-order", "stroke fill markers");
        } else {
            _setStrokeStyle(css, state);
        }
        sp_repr_css_change(prev, css, "style");
        sp_repr_css_attr_unref(css);
        return true;
    }
    return false;
}

/**
 * \brief Emits the current path in poppler's GfxState data structure
 * Can be used to do filling and stroking at once.
 *
 * \param fill whether the path should be filled
 * \param stroke whether the path should be stroked
 * \param even_odd whether the even-odd rule should be used when filling the path
 */
void SvgBuilder::addPath(GfxState *state, bool fill, bool stroke, bool even_odd) {
    gchar *pathtext = svgInterpretPath(state->getPath());

    if (!pathtext)
        return;

    if (!strlen(pathtext) || (fill != stroke && mergePath(state, fill, pathtext, even_odd))) {
        g_free(pathtext);
        return;
    }

    Inkscape::XML::Node *path = _addToContainer("svg:path");
    path->setAttribute("d", pathtext);
    g_free(pathtext);

    // Set style
    SPCSSAttr *css = _setStyle(state, fill, stroke, even_odd);
    sp_repr_css_change(path, css, "style");
    sp_repr_css_attr_unref(css);
    _setBlendMode(path, state);
    _setTransform(path, state);
    _setClipPath(path);
}

void SvgBuilder::addClippedFill(GfxShading *shading, const Geom::Affine shading_tr)
{
    if (_clip_history->getClipPath()) {
        addShadedFill(shading, shading_tr, _clip_history->getClipPath(), _clip_history->getAffine(),
                      _clip_history->getClipType() == clipEO);
    }
}

/**
 * \brief Emits the current path in poppler's GfxState data structure
 * The path is set to be filled with the given shading.
 */
void SvgBuilder::addShadedFill(GfxShading *shading, const Geom::Affine shading_tr, GfxPath *path, const Geom::Affine tr,
                               bool even_odd)
{
    auto prev = _container->lastChild();
    gchar *pathtext = svgInterpretPath(path);

    // Create a new gradient object before comitting to creating a path for it
    // And package it into a css bundle which can be applied
    SPCSSAttr *css = sp_repr_css_attr_new();
    // We remove the shape's affine to adjust the gradient back into place
    gchar *id = _createGradient(shading, shading_tr * tr.inverse(), true);
    if (id) {
        gchar *urltext = g_strdup_printf ("url(#%s)", id);
        sp_repr_css_set_property(css, "fill", urltext);
        g_free(urltext);
        g_free(id);
    } else {
        sp_repr_css_attr_unref(css);
        return;
    }
    if (even_odd) {
        sp_repr_css_set_property(css, "fill-rule", "evenodd");
    }
    // Merge the style with the previous shape
    if (shouldMergePath(true, pathtext)) {
        // POSSIBLE: The gradientTransform might now incorrect if the
        // state of the transformation was different between the two paths.
        sp_repr_css_change(prev, css, "style");
        g_free(pathtext);
        return;
    }

    Inkscape::XML::Node *path_node = _addToContainer("svg:path");
    path_node->setAttribute("d", pathtext);
    g_free(pathtext);

    // Don't add transforms to mask children.
    if (std::string("svg:mask") != _container->name()) {
        svgSetTransform(path_node, tr * _page_affine);
    }

    // Set the gradient into this new path.
    sp_repr_css_set_property(css, "stroke", "none");
    sp_repr_css_change(path_node, css, "style");
    sp_repr_css_attr_unref(css);
}

/**
 * \brief Clips to the current path set in GfxState
 * \param state poppler's data structure
 * \param even_odd whether the even-odd rule should be applied
 */
void SvgBuilder::setClip(GfxState *state, GfxClipType clip, bool is_bbox)
{
    // When there's already a clip path, we add clipping groups to handle them.
    if (!is_bbox && _clip_history->hasClipPath() && !_clip_history->isCopied()) {
        _pushContainer("svg:g");
        _clip_groups++;
    }
    if (clip == clipNormal) {
        _clip_history->setClip(state, clipNormal, is_bbox);
    } else {
        _clip_history->setClip(state, clipEO);
    }
}

/**
 * Return the active clip as a new xml node.
 */
Inkscape::XML::Node *SvgBuilder::_getClip(const Geom::Affine &node_tr)
{
    // In SVG the path-clip transforms are compounded, so we have to do extra work to
    // pull transforms back out of the clipping object and set them. Otherwise this
    // would all be a lot simpler.
    if (_clip_text) {
        auto node = _clip_text;

        auto text_tr = Geom::identity();
        if (auto attr = node->attribute("transform")) {
            sp_svg_transform_read(attr, &text_tr);
            node->removeAttribute("transform");
        }

        for (auto child = node->firstChild(); child; child = child->next()) {
            Geom::Affine child_tr = text_tr * _page_affine * node_tr.inverse();
            svgSetTransform(child, child_tr);
        }

        _clip_text = nullptr;
        return node;
    }
    if (_clip_history->hasClipPath()) {
        std::string clip_d = svgInterpretPath(_clip_history->getClipPath());
        Geom::Affine tr = _clip_history->getAffine() * _page_affine * node_tr.inverse();
        return _createClip(clip_d, tr, _clip_history->evenOdd());
    }
    return nullptr;
}

Inkscape::XML::Node *SvgBuilder::_createClip(const std::string &d, const Geom::Affine tr, bool even_odd)
{
    Inkscape::XML::Node *clip_path = _xml_doc->createElement("svg:clipPath");
    clip_path->setAttribute("clipPathUnits", "userSpaceOnUse");

    // Create the path
    Inkscape::XML::Node *path = _xml_doc->createElement("svg:path");
    path->setAttribute("d", d);
    svgSetTransform(path, tr);

    if (even_odd) {
        path->setAttribute("clip-rule", "evenodd");
    }
    clip_path->appendChild(path);
    Inkscape::GC::release(path);

    // Append clipPath to defs and get id
    _doc->getDefs()->getRepr()->appendChild(clip_path);
    Inkscape::GC::release(clip_path);
    return clip_path;
}

void SvgBuilder::beginMarkedContent(const char *name, const char *group)
{
    if (name && group && std::string(name) == "OC") {
        auto layer_id = std::string("layer-") + group;
        if (auto existing = _doc->getObjectById(layer_id)) {
            if (existing->getRepr()->parent() == _container) {
                _container = existing->getRepr();
                _node_stack.push_back(_container);
            } else {
                g_warning("Unexpected marked content group in PDF!");
                _pushGroup();
            }
        } else {
            auto node = _pushGroup();
            node->setAttribute("id", layer_id);
            if (_ocgs.find(group) != _ocgs.end()) {
                auto pair = _ocgs[group];
                setAsLayer(pair.first.c_str(), pair.second);
            }
        }
    } else {
        auto node = _pushGroup();
        if (group) {
            node->setAttribute("id", std::string("group-") + group);
        }
    }
}

void SvgBuilder::addOptionalGroup(const std::string &oc, const std::string &label, bool visible)
{
    _ocgs[oc] = {label, visible};
}

void SvgBuilder::endMarkedContent()
{
    _popGroup();
}

void SvgBuilder::addColorProfile(unsigned char *profBuf, int length)
{
    cmsHPROFILE hp = cmsOpenProfileFromMem(profBuf, length);
    if (!hp) {
        g_warning("Failed to read ICCBased color space profile from PDF file.");
        return;
    }
    _icc_profile = _getColorProfile(hp);
}

/**
 * Return the color profile name if it's already been added
 */
std::string SvgBuilder::_getColorProfile(cmsHPROFILE hp)
{
    if (!hp)
        return "";

    // Cached name of this profile by reference
    if (_icc_profiles.find(hp) != _icc_profiles.end())
        return _icc_profiles[hp];

    std::string name = Inkscape::ColorProfile::getNameFromProfile(hp);
    Inkscape::ColorProfile::sanitizeName(name);

    // Find the named profile in the document (if already added)
    if (_doc->getProfileManager().find(name.c_str()))
        return name;

    // Add the profile, we've never seen it before.
    cmsUInt32Number len = 0;
    cmsSaveProfileToMem(hp, nullptr, &len);
    auto buf = (unsigned char *)malloc(len * sizeof(unsigned char));
    cmsSaveProfileToMem(hp, buf, &len);

    Inkscape::XML::Node *icc_node = _xml_doc->createElement("svg:color-profile");
    std::string label = Inkscape::ColorProfile::getNameFromProfile(hp);
    icc_node->setAttribute("inkscape:label", label);
    icc_node->setAttribute("name", name);

    auto *base64String = g_base64_encode(buf, len);
    auto icc_data = std::string("data:application/vnd.iccprofile;base64,") + base64String;
    g_free(base64String);
    icc_node->setAttributeOrRemoveIfEmpty("xlink:href", icc_data);
    _doc->getDefs()->getRepr()->appendChild(icc_node);
    Inkscape::GC::release(icc_node);

    free(buf);
    _icc_profiles[hp] = name;
    return name;
}

/**
 * \brief Checks whether the given pattern type can be represented in SVG
 * Used by PdfParser to decide when to do fallback operations.
 */
bool SvgBuilder::isPatternTypeSupported(GfxPattern *pattern) {
    if ( pattern != nullptr ) {
        if ( pattern->getType() == 2 ) {    // shading pattern
            GfxShading *shading = (static_cast<GfxShadingPattern *>(pattern))->getShading();
            int shadingType = shading->getType();
            if ( shadingType == 2 || // axial shading
                 shadingType == 3 ) {   // radial shading
                return true;
            }
            return false;
        } else if ( pattern->getType() == 1 ) {   // tiling pattern
            return true;
        }
    }

    return false;
}

/**
 * \brief Creates a pattern from poppler's data structure
 * Handles linear and radial gradients. Creates a new PdfParser and uses it to
 * build a tiling pattern.
 * \return a url pointing to the created pattern
 */
gchar *SvgBuilder::_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke) {
    gchar *id = nullptr;
    if ( pattern != nullptr ) {
        if ( pattern->getType() == 2 ) {  // Shading pattern
            GfxShadingPattern *shading_pattern = static_cast<GfxShadingPattern *>(pattern);
            // construct a (pattern space) -> (current space) transform matrix
            auto flip = Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, _height);
            auto pt = Geom::Scale(Inkscape::Util::Quantity::convert(1.0, "pt", "px"));
            auto grad_affine = ctmToAffine(shading_pattern->getMatrix());
            auto obj_affine = stateToAffine(state);
            // SVG applies the object's affine on top of the gradient's affine,
            // So we must remove the object affine to move it back into place.
            auto affine = (grad_affine * pt * flip) * obj_affine.inverse();
            id = _createGradient(shading_pattern->getShading(), affine, !is_stroke);
        } else if ( pattern->getType() == 1 ) {   // Tiling pattern
            id = _createTilingPattern(static_cast<GfxTilingPattern*>(pattern), state, is_stroke);
        }
    } else {
        return nullptr;
    }
    gchar *urltext = g_strdup_printf ("url(#%s)", id);
    g_free(id);
    return urltext;
}

/**
 * \brief Creates a tiling pattern from poppler's data structure
 * Creates a sub-page PdfParser and uses it to parse the pattern's content stream.
 * \return id of the created pattern
 */
gchar *SvgBuilder::_createTilingPattern(GfxTilingPattern *tiling_pattern,
                                        GfxState *state, bool is_stroke) {

    Inkscape::XML::Node *pattern_node = _xml_doc->createElement("svg:pattern");
    // Set pattern transform matrix
    auto pat_matrix = ctmToAffine(tiling_pattern->getMatrix());
    pattern_node->setAttributeOrRemoveIfEmpty("patternTransform", sp_svg_transform_write(pat_matrix));
    pattern_node->setAttribute("patternUnits", "userSpaceOnUse");
    // Set pattern tiling
    // FIXME: don't ignore XStep and YStep
    const double *bbox = tiling_pattern->getBBox();
    pattern_node->setAttributeSvgDouble("x", 0.0);
    pattern_node->setAttributeSvgDouble("y", 0.0);
    pattern_node->setAttributeSvgDouble("width", bbox[2] - bbox[0]);
    pattern_node->setAttributeSvgDouble("height", bbox[3] - bbox[1]);

    // Convert BBox for PdfParser
    PDFRectangle box;
    box.x1 = bbox[0];
    box.y1 = bbox[1];
    box.x2 = bbox[2];
    box.y2 = bbox[3];
    // Create new SvgBuilder and sub-page PdfParser
    SvgBuilder *pattern_builder = new SvgBuilder(this, pattern_node);
    PdfParser *pdf_parser = new PdfParser(_xref, pattern_builder, tiling_pattern->getResDict(),
                                          &box);
    // Get pattern color space
    GfxPatternColorSpace *pat_cs = (GfxPatternColorSpace *)( is_stroke ? state->getStrokeColorSpace()
                                                            : state->getFillColorSpace() );
    // Set fill/stroke colors if this is an uncolored tiling pattern
    GfxColorSpace *cs = nullptr;
    if ( tiling_pattern->getPaintType() == 2 && ( cs = pat_cs->getUnder() ) ) {
        GfxState *pattern_state = pdf_parser->getState();
        pattern_state->setFillColorSpace(cs->copy());
        pattern_state->setFillColor(state->getFillColor());
        pattern_state->setStrokeColorSpace(cs->copy());
        pattern_state->setStrokeColor(state->getFillColor());
    }

    // Generate the SVG pattern
    pdf_parser->parse(tiling_pattern->getContentStream());

    // Cleanup
    delete pdf_parser;
    delete pattern_builder;

    // Append the pattern to defs
    _doc->getDefs()->getRepr()->appendChild(pattern_node);
    gchar *id = g_strdup(pattern_node->attribute("id"));
    Inkscape::GC::release(pattern_node);

    return id;
}

/**
 * \brief Creates a linear or radial gradient from poppler's data structure
 * \param shading poppler's data structure for the shading
 * \param matrix gradient transformation, can be null
 * \param for_shading true if we're creating this for a shading operator; false otherwise
 * \return id of the created object
 */
gchar *SvgBuilder::_createGradient(GfxShading *shading, const Geom::Affine pat_matrix, bool for_shading)
{
    Inkscape::XML::Node *gradient;
    _POPPLER_CONST Function *func;
    int num_funcs;
    bool extend0, extend1;

    if ( shading->getType() == 2 ) {  // Axial shading
        gradient = _xml_doc->createElement("svg:linearGradient");
        GfxAxialShading *axial_shading = static_cast<GfxAxialShading*>(shading);
        double x1, y1, x2, y2;
        axial_shading->getCoords(&x1, &y1, &x2, &y2);
        gradient->setAttributeSvgDouble("x1", x1);
        gradient->setAttributeSvgDouble("y1", y1);
        gradient->setAttributeSvgDouble("x2", x2);
        gradient->setAttributeSvgDouble("y2", y2);
        extend0 = axial_shading->getExtend0();
        extend1 = axial_shading->getExtend1();
        num_funcs = axial_shading->getNFuncs();
        func = axial_shading->getFunc(0);
    } else if (shading->getType() == 3) {   // Radial shading
        gradient = _xml_doc->createElement("svg:radialGradient");
        GfxRadialShading *radial_shading = static_cast<GfxRadialShading*>(shading);
        double x1, y1, r1, x2, y2, r2;
        radial_shading->getCoords(&x1, &y1, &r1, &x2, &y2, &r2);
        // FIXME: the inner circle's radius is ignored here
        gradient->setAttributeSvgDouble("fx", x1);
        gradient->setAttributeSvgDouble("fy", y1);
        gradient->setAttributeSvgDouble("cx", x2);
        gradient->setAttributeSvgDouble("cy", y2);
        gradient->setAttributeSvgDouble("r", r2);
        extend0 = radial_shading->getExtend0();
        extend1 = radial_shading->getExtend1();
        num_funcs = radial_shading->getNFuncs();
        func = radial_shading->getFunc(0);
    } else {    // Unsupported shading type
        return nullptr;
    }
    gradient->setAttribute("gradientUnits", "userSpaceOnUse");
    // If needed, flip the gradient transform around the y axis
    if (pat_matrix != Geom::identity()) {
        gradient->setAttributeOrRemoveIfEmpty("gradientTransform", sp_svg_transform_write(pat_matrix));
    }

    if ( extend0 && extend1 ) {
        gradient->setAttribute("spreadMethod", "pad");
    }

    if ( num_funcs > 1 || !_addGradientStops(gradient, shading, func) ) {
        Inkscape::GC::release(gradient);
        return nullptr;
    }

    _doc->getDefs()->getRepr()->appendChild(gradient);
    gchar *id = g_strdup(gradient->attribute("id"));
    Inkscape::GC::release(gradient);

    return id;
}

#define EPSILON 0.0001
/**
 * \brief Adds a stop with the given properties to the gradient's representation
 */
void SvgBuilder::_addStopToGradient(Inkscape::XML::Node *gradient, double offset, GfxColor *color, GfxColorSpace *space,
                                    double opacity)
{
    Inkscape::XML::Node *stop = _xml_doc->createElement("svg:stop");
    SPCSSAttr *css = sp_repr_css_attr_new();
    Inkscape::CSSOStringStream os_opacity;
    std::string color_text = "#ffffff";
    if (space->getMode() == csDeviceGray) {
        // This is a transparency mask.
        GfxRGB rgb;
        space->getRGB(color, &rgb);
        double gray = (double)rgb.r / 65535.0;
        gray = CLAMP(gray, 0.0, 1.0);
        os_opacity << gray;
    } else {
        os_opacity << opacity;
        color_text = convertGfxColor(color, space);
    }
    sp_repr_css_set_property(css, "stop-opacity", os_opacity.str().c_str());
    sp_repr_css_set_property(css, "stop-color", color_text.c_str());

    sp_repr_css_change(stop, css, "style");
    sp_repr_css_attr_unref(css);
    stop->setAttributeCssDouble("offset", offset);

    gradient->appendChild(stop);
    Inkscape::GC::release(stop);
}

static bool svgGetShadingColor(GfxShading *shading, double offset, GfxColor *result)
{
    if ( shading->getType() == 2 ) {  // Axial shading
        (static_cast<GfxAxialShading *>(shading))->getColor(offset, result);
    } else if ( shading->getType() == 3 ) { // Radial shading
        (static_cast<GfxRadialShading *>(shading))->getColor(offset, result);
    } else {
        return false;
    }
    return true;
}

#define INT_EPSILON 8
bool SvgBuilder::_addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading,
                                   _POPPLER_CONST Function *func) {
    int type = func->getType();
    auto space = shading->getColorSpace();
    if ( type == 0 || type == 2 ) {  // Sampled or exponential function
        GfxColor stop1, stop2;
        if (!svgGetShadingColor(shading, 0.0, &stop1) || !svgGetShadingColor(shading, 1.0, &stop2)) {
            return false;
        } else {
            _addStopToGradient(gradient, 0.0, &stop1, space, 1.0);
            _addStopToGradient(gradient, 1.0, &stop2, space, 1.0);
        }
    } else if ( type == 3 ) { // Stitching
        auto stitchingFunc = static_cast<_POPPLER_CONST StitchingFunction*>(func);
        const double *bounds = stitchingFunc->getBounds();
        const double *encode = stitchingFunc->getEncode();
        int num_funcs = stitchingFunc->getNumFuncs();
        // Adjust gradient so it's always between 0.0 - 1.0
        double max_bound = std::max({1.0, bounds[num_funcs]});

        // Add stops from all the stitched functions
        GfxColor prev_color, color;
        svgGetShadingColor(shading, bounds[0], &prev_color);
        _addStopToGradient(gradient, bounds[0], &prev_color, space, 1.0);
        for ( int i = 0 ; i < num_funcs ; i++ ) {
            svgGetShadingColor(shading, bounds[i + 1], &color);
            // Add stops
            if (stitchingFunc->getFunc(i)->getType() == 2) {    // process exponential fxn
                double expE = (static_cast<_POPPLER_CONST ExponentialFunction*>(stitchingFunc->getFunc(i)))->getE();
                if (expE > 1.0) {
                    expE = (bounds[i + 1] - bounds[i])/expE;    // approximate exponential as a single straight line at x=1
                    if (encode[2*i] == 0) {    // normal sequence
                        auto offset = (bounds[i + 1] - expE) / max_bound;
                        _addStopToGradient(gradient, offset, &prev_color, space, 1.0);
                    } else {                   // reflected sequence
                        auto offset = (bounds[i] + expE) / max_bound;
                        _addStopToGradient(gradient, offset, &color, space, 1.0);
                    }
                }
            }
            _addStopToGradient(gradient, bounds[i + 1] / max_bound, &color, space, 1.0);
            prev_color = color;
        }
    } else { // Unsupported function type
        return false;
    }

    return true;
}

/**
 * \brief Sets _invalidated_style to true to indicate that styles have to be updated
 * Used for text output when glyphs are buffered till a font change
 */
void SvgBuilder::updateStyle(GfxState *state) {
    if (_in_text_object) {
        _invalidated_style = true;
    }
}

/**
 * \brief Updates _css_font according to the font set in parameter state
 */
void SvgBuilder::updateFont(GfxState *state, std::shared_ptr<CairoFont> cairo_font, bool flip)
{
    TRACE(("updateFont()\n"));
    updateTextMatrix(state, flip);    // Ensure that we have a text matrix built

    auto font = state->getFont();
    auto font_id = font->getID()->num;

    auto new_font_size = state->getFontSize();
    if (font->getType() == fontType3) {
        const double *font_matrix = font->getFontMatrix();
        if (font_matrix[0] != 0.0) {
            new_font_size *= font_matrix[3] / font_matrix[0];
        }
    }
    if (new_font_size != _css_font_size) {
        _css_font_size = new_font_size;
        _invalidated_style = true;
    }
    bool was_css_font = (bool)_css_font;
    // Clean up any previous css font
    if (_css_font) {
        sp_repr_css_attr_unref(_css_font);
        _css_font = nullptr;
    }

    auto font_strategy = FontFallback::AS_TEXT;
    if (_font_strategies.find(font_id) != _font_strategies.end()) {
        font_strategy = _font_strategies[font_id];
    }

    if (font_strategy == FontFallback::DELETE_TEXT) {
        _invalidated_strategy = true;
        _cairo_font = nullptr;
        return;
    }
    if (font_strategy == FontFallback::AS_SHAPES) {
        _invalidated_strategy = _invalidated_strategy || was_css_font;
        _invalidated_style = (_cairo_font != cairo_font);
        _cairo_font = cairo_font;
        return;
    }

    auto font_data = FontData(font);
    _font_specification = font_data.getSpecification().c_str();
    _invalidated_strategy = (bool)_cairo_font;
    _invalidated_style = true;

    // Font family
    _cairo_font = nullptr;
    _css_font = sp_repr_css_attr_new();
    if (font_data.found) {
        sp_repr_css_set_property(_css_font, "font-family", font_data.family.c_str());
    } else if (font_strategy == FontFallback::AS_SUB) {
        sp_repr_css_set_property(_css_font, "font-family", font_data.getSubstitute().c_str());
    } else {
        auto keep_name = font_data.family.size() ? font_data.family : font_data.name;
        sp_repr_css_set_property(_css_font, "font-family", keep_name.c_str());
    }

    // Set the font data
    sp_repr_css_set_property(_css_font, "font-style", font_data.style.c_str());
    sp_repr_css_set_property(_css_font, "font-weight", font_data.weight.c_str());
    sp_repr_css_set_property(_css_font, "font-stretch", font_data.stretch.c_str());
    sp_repr_css_set_property(_css_font, "font-variant", "normal");

    // Writing mode
    if ( font->getWMode() == 0 ) {
        sp_repr_css_set_property(_css_font, "writing-mode", "lr");
    } else {
        sp_repr_css_set_property(_css_font, "writing-mode", "tb");
    }
}

/**
 * \brief Shifts the current text position by the given amount (specified in text space)
 */
void SvgBuilder::updateTextShift(GfxState *state, double shift) {
    double shift_value = -shift * 0.001 * fabs(state->getFontSize());
    if (state->getFont()->getWMode()) {
        _text_position[1] += shift_value;
    } else {
        _text_position[0] += shift_value;
    }
}

/**
 * \brief Updates current text position
 */
void SvgBuilder::updateTextPosition(double tx, double ty) {
    _text_position = Geom::Point(tx, ty);
}

/**
 * \brief Flushes the buffered characters
 */
void SvgBuilder::updateTextMatrix(GfxState *state, bool flip) {
    // Update text matrix, it contains an extra flip which we must undo.
    auto new_matrix = Geom::Scale(1, flip ? -1 : 1) * ctmToAffine(state->getTextMat());
    // TODO: Detect if the text matrix is actually just a rotational kern
    // this can help stich back together texts where letters are rotated
    if (new_matrix != _text_matrix) {
        _flushText(state);
        _text_matrix = new_matrix;
    }
}

/**
 * \brief Notifies the svg builder the state will change
 *
 * Used to flushText if we are in text object
*/
void SvgBuilder::beforeStateChange(GfxState *old_state) {
    if (_in_text_object) {
        _flushText(old_state);
    }
}

/**
 * \brief Writes the buffered characters to the SVG document
 *
 * This is a dual path function that can produce either a text element
 * or a group of path elements depending on the font handling mode.
 */
void SvgBuilder::_flushText(GfxState *state)
{
    // Set up a clipPath group
    if (state->getRender() & 4 && !_clip_text_group) {
        auto defs = _doc->getDefs()->getRepr();
        _clip_text_group = _pushContainer("svg:clipPath");
        _clip_text_group->setAttribute("clipPathUnits", "userSpaceOnUse");
        defs->appendChild(_clip_text_group);
        Inkscape::GC::release(_clip_text_group);
    }

    // Ignore empty strings
    if (_glyphs.empty()) {
        _glyphs.clear();
        return;
    }
    std::vector<SvgGlyph>::iterator i = _glyphs.begin();
    const SvgGlyph& first_glyph = (*i);

    // Ignore invisible characters
    if (first_glyph.state->getRender() == 3) {
        _glyphs.clear();
        return;
    }

    // If cairo, then no text node is needed.
    Inkscape::XML::Node *text_group = nullptr;
    Inkscape::XML::Node *text_node = nullptr;
    cairo_glyph_t *cairo_glyphs = nullptr;
    unsigned int cairo_glyph_count = 0;

    if (!first_glyph.cairo_font) {
        // we preserve spaces in the text objects we create, this applies to any descendant
        text_node = _addToContainer("svg:text");
        text_node->setAttribute("xml:space", "preserve");
    }

    // Strip out text size from text_matrix and remove from text_transform
    double text_scale = _text_matrix.expansionX();
    Geom::Affine tr = stateToAffine(state);
    Geom::Affine text_transform = _text_matrix * tr * Geom::Scale(text_scale).inverse();
    // The glyph position must be moved by the document scale without flipping
    // the text object itself. This is why the text affine is applied to the
    // translation point and not simply used in the text element directly.
    auto pos = first_glyph.position * tr;
    text_transform.setTranslation(pos);
    // Cache the text transform when clipping
    if (_clip_text_group) {
        svgSetTransform(_clip_text_group, text_transform);
    }

    bool new_tspan = true;
    bool same_coords[2] = {true, true};
    Geom::Point last_delta_pos;
    unsigned int glyphs_in_a_row = 0;
    Inkscape::XML::Node *tspan_node = nullptr;
    Glib::ustring x_coords;
    Glib::ustring y_coords;
    Glib::ustring text_buffer;

    // Output all buffered glyphs
    while (true) {
        const SvgGlyph& glyph = (*i);
        auto prev_iterator = (i == _glyphs.begin()) ? _glyphs.end() : (i-1);
        // Check if we need to make a new tspan
        if (glyph.style_changed) {
            new_tspan = true;
        } else if ( i != _glyphs.begin() ) {
            const SvgGlyph& prev_glyph = (*prev_iterator);
            if (!((glyph.delta[Geom::Y] == 0.0 && prev_glyph.delta[Geom::Y] == 0.0 &&
                   glyph.text_position[1] == prev_glyph.text_position[1]) ||
                  (glyph.delta[Geom::X] == 0.0 && prev_glyph.delta[Geom::X] == 0.0 &&
                   glyph.text_position[0] == prev_glyph.text_position[0]))) {
                new_tspan = true;
            }
        }

        // Create tspan node if needed
        if (!first_glyph.cairo_font && text_node && (new_tspan || i == _glyphs.end())) {
            if (tspan_node) {
                // Set the x and y coordinate arrays
                if (same_coords[0]) {
                    tspan_node->setAttributeSvgDouble("x", last_delta_pos[0]);
                } else {
                    tspan_node->setAttributeOrRemoveIfEmpty("x", x_coords);
                }
                if (same_coords[1]) {
                    tspan_node->setAttributeSvgDouble("y", last_delta_pos[1]);
                } else {
                    tspan_node->setAttributeOrRemoveIfEmpty("y", y_coords);
                }
                TRACE(("tspan content: %s\n", text_buffer.c_str()));
                if ( glyphs_in_a_row > 1 ) {
                    tspan_node->setAttribute("sodipodi:role", "line");
                }
                // Add text content node to tspan
                Inkscape::XML::Node *text_content = _xml_doc->createTextNode(text_buffer.c_str());
                tspan_node->appendChild(text_content);
                Inkscape::GC::release(text_content);
                text_node->appendChild(tspan_node);
                // Clear temporary buffers
                x_coords.clear();
                y_coords.clear();
                text_buffer.clear();
                Inkscape::GC::release(tspan_node);
                glyphs_in_a_row = 0;
            }
            if ( i == _glyphs.end() ) {
                sp_repr_css_attr_unref((*prev_iterator).css_font);
                break;
            } else {
                tspan_node = _xml_doc->createElement("svg:tspan");

                // Set style and unref SPCSSAttr if it won't be needed anymore
                // assume all <tspan> nodes in a <text> node share the same style
                double text_size = text_scale * glyph.text_size;
                sp_repr_css_set_property_double(glyph.css_font, "font-size", text_size);
                _setTextStyle(tspan_node, glyph.state, glyph.css_font, text_transform);
                if ( glyph.style_changed && i != _glyphs.begin() ) {    // Free previous style
                    sp_repr_css_attr_unref((*prev_iterator).css_font);
                }
            }
            new_tspan = false;
        }
        if ( glyphs_in_a_row > 0 && i != _glyphs.begin() ) {
            x_coords.append(" ");
            y_coords.append(" ");
            // Check if we have the same coordinates
            const SvgGlyph& prev_glyph = (*prev_iterator);
            for ( int p = 0 ; p < 2 ; p++ ) {
                if ( glyph.text_position[p] != prev_glyph.text_position[p] ) {
                    same_coords[p] = false;
                }
            }
        }
        // Append the coordinates to their respective strings
        Geom::Point delta_pos(glyph.text_position - first_glyph.text_position);
        delta_pos[1] += glyph.rise;
        delta_pos[1] *= -1.0;   // flip it
        delta_pos *= Geom::Scale(text_scale);
        Inkscape::CSSOStringStream os_x;
        os_x << delta_pos[0];
        x_coords.append(os_x.str());
        Inkscape::CSSOStringStream os_y;
        os_y << delta_pos[1];
        y_coords.append(os_y.str());
        last_delta_pos = delta_pos;

        if (first_glyph.cairo_font) {
            if (!cairo_glyphs) {
                cairo_glyphs = (cairo_glyph_t *)gmallocn(_glyphs.size(), sizeof(cairo_glyph_t));
            }
            bool is_last_glyph = i + 1 == _glyphs.end();

            // Push the data into the cairo glyph list for later rendering.
            cairo_glyphs[cairo_glyph_count].index = glyph.cairo_index;
            cairo_glyphs[cairo_glyph_count].x = delta_pos[Geom::X];
            cairo_glyphs[cairo_glyph_count].y = delta_pos[Geom::Y];
            cairo_glyph_count++;

            bool style_will_change = is_last_glyph ? true : (i+1)->style_changed;
            if (style_will_change) {
                if (style_will_change && !is_last_glyph && !text_group) {
                    // We create a group, so each style can be contained within the resulting path.
                    text_group = _pushGroup();
                }

                // Render and set the style for this drawn text.
                double text_size = text_scale * glyph.text_size;

                // Set to 'text_node' because if the style does NOT change, we won't have a group
                // but still need to set this text's position and blend modes.
                text_node = _renderText(glyph.cairo_font, text_size, text_transform, cairo_glyphs, cairo_glyph_count);
                if (text_node) {
                    _setTextStyle(text_node, glyph.state, nullptr, text_transform);
                }

                // Free up the used glyph stack.
                gfree(cairo_glyphs);
                cairo_glyphs = nullptr;
                cairo_glyph_count = 0;

                if (is_last_glyph) {
                    // Stop drawing text now, we have cleaned up.
                    break;
                }
            }
        } else {
            // Append the character to the text buffer
            if (!glyph.code.empty()) {
                text_buffer.append(1, glyph.code[0]);
            }

            /* Append any utf8 conversion doublets and request a new tspan.
             *
             * This is a fix for the unusual situation in some PDF files that use
             * certain fonts where two ascii letters have been bolted together into
             * one Unicode position and our conversion to UTF8 produces extra glyphs
             * which if we don't add will be missing and if we add without ending the
             * tspan will cause the rest of the glyph-positions to be off by one.
             */
            for (int j = 1; j < glyph.code.size(); j++) {
                text_buffer.append(1, glyph.code[j]);
                new_tspan = true;
            }
        }

        glyphs_in_a_row++;
        ++i;
    }
    if (text_group) {
        // Pop the group so the clip and transform can be applied to it.
        text_node = text_group;
        _popGroup();
    }

    if (text_node) {
        if (first_glyph.cairo_font) {
            // Save aria-label for any rendered text blocks
            text_node->setAttribute("aria-label", _aria_label);
        }

        // Set the text matrix which sits under the page's position
        _setBlendMode(text_node, state);
        svgSetTransform(text_node, text_transform * _page_affine);
        _setClipPath(text_node);
    }

    _aria_label = "";
    _glyphs.clear();
}

/**
 * Sets the style for the text, rendered or un-rendered, preserving the text_transform for any
 * gradients or other patterns. These values were promised to us when the font was updated.
 */
void SvgBuilder::_setTextStyle(Inkscape::XML::Node *node, GfxState *state, SPCSSAttr *font_style, Geom::Affine ta)
{
    int render_mode = state->getRender();
    bool has_fill = !(render_mode & 1);
    bool has_stroke = ( render_mode & 3 ) == 1 || ( render_mode & 3 ) == 2;

    state = state->save();
    state->setCTM(ta[0], ta[1], ta[2], ta[3], ta[4], ta[5]);
    auto style = _setStyle(state, has_fill, has_stroke);
    state = state->restore();
    if (font_style) {
        sp_repr_css_merge(style, font_style);
    }
    sp_repr_css_change(node, style, "style");
    sp_repr_css_attr_unref(style);
}

/**
 * Renders the text as a path object using cairo and returns the node object.
 *
 * cairo_font   - The font that cairo can use to convert text to path.
 * font_size    - The size of the text when drawing the path.
 * transform    - The matrix which will place the text on the page, this is critical
 *                to allow cairo to render all the required parts of the text.
 * cairo_glyphs - A pointer to a list of glyphs to render.
 * count        - A count of the number of glyphs to render.
 */
Inkscape::XML::Node *SvgBuilder::_renderText(std::shared_ptr<CairoFont> cairo_font, double font_size,
                                             const Geom::Affine &transform,
                                             cairo_glyph_t *cairo_glyphs, unsigned int count)
{
    if (!cairo_glyphs || !cairo_font || _aria_label.empty())
        return nullptr;

    // The surface isn't actually used, no rendering in cairo takes place.
    cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _width, _height);
    cairo_t *cairo = cairo_create(surface);
    cairo_set_font_face(cairo, cairo_font->getFontFace());
    cairo_set_font_size(cairo, font_size);
    ink_cairo_transform(cairo, transform);
    cairo_glyph_path(cairo, cairo_glyphs, count);
    auto pathv = extract_pathvector_from_cairo(cairo);
    cairo_destroy(cairo);
    cairo_surface_destroy(surface);

    // Failing to render text.
    if (!pathv) {
        g_warning("Failed to render PDF text!");
        return nullptr;
    }

    auto textpath = sp_svg_write_path(*pathv);
    if (textpath.empty())
        return nullptr;

    Inkscape::XML::Node *path = _addToContainer("svg:path");
    path->setAttribute("d", textpath);
    return path;
}

/**
 * Begin and end string is the inner most text processing step
 * which tells us we're about to have a certain number of chars.
 */
void SvgBuilder::beginString(GfxState *state, int len)
{
    if (!_glyphs.empty()) {
        // What to do about unflushed text in the buffer.
        if (_invalidated_strategy) {
            _flushText(state);
            _invalidated_strategy = false;
        } else {
            // Add seperator for aria text.
            _aria_space = true;
        }
    }
    IFTRACE(double *m = state->getTextMat());
    TRACE(("tm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5]));
    IFTRACE(m = state->getCTM());
    TRACE(("ctm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5]));
}
void SvgBuilder::endString(GfxState *state)
{
}

/**
 * \brief Adds the specified character to the text buffer
 * Takes care of converting it to UTF-8 and generates a new style repr if style
 * has changed since the last call.
 */
void SvgBuilder::addChar(GfxState *state, double x, double y, double dx, double dy, double originX, double originY,
                         CharCode code, int /*nBytes*/, Unicode const *u, int uLen)
{
    if (_aria_space && !_glyphs.empty()) {
        const SvgGlyph& prev_glyph = _glyphs.back();
        // This helps reconstruct the aria text, though it could be made better
        if (prev_glyph.position[Geom::Y] != (y - originY)) {
            _aria_label += "\n";
        }
    }
    _aria_space = false;

    static std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> conv1;
    if (u) {
        _aria_label += conv1.to_bytes(*u);
    }

    // Skip control characters, found in LaTeX generated PDFs
    // https://gitlab.com/inkscape/inkscape/-/issues/1369
    if (uLen > 0 && u[0] < 0x80 && g_ascii_iscntrl(u[0]) && !g_ascii_isspace(u[0])) {
        g_warning("Skipping ASCII control character %u", u[0]);
        _text_position += Geom::Point(dx, dy);
        return;
    }

    if (!_css_font && !_cairo_font) {
        // Deleted text.
        return;
    }

    bool is_space = ( uLen == 1 && u[0] == 32 );
    // Skip beginning space
    if ( is_space && _glyphs.empty()) {
        Geom::Point delta(dx, dy);
         _text_position += delta;
         return;
    }
    // Allow only one space in a row
    if ( is_space && (_glyphs[_glyphs.size() - 1].code.size() == 1) &&
         (_glyphs[_glyphs.size() - 1].code[0] == 32) ) {
        Geom::Point delta(dx, dy);
        _text_position += delta;
        return;
    }

    SvgGlyph new_glyph;
    new_glyph.is_space = is_space;
    new_glyph.delta = Geom::Point(dx, dy);
    new_glyph.position = Geom::Point( x - originX, y - originY );
    new_glyph.text_position = _text_position;
    new_glyph.text_size = _css_font_size;
    new_glyph.state = state;
    if (_cairo_font) {
        new_glyph.cairo_font = _cairo_font;
        new_glyph.cairo_index = _cairo_font->getGlyph(code, u, uLen);
    }
    _text_position += new_glyph.delta;

    // Convert the character to UTF-8 since that's our SVG document's encoding
    {
        gunichar2 uu[8] = {0};

        for (int i = 0; i < uLen; i++) {
            uu[i] = u[i];
        }

        gchar *tmp = g_utf16_to_utf8(uu, uLen, nullptr, nullptr, nullptr);
        if ( tmp && *tmp ) {
            new_glyph.code = tmp;
        } else {
            new_glyph.code.clear();
        }
        g_free(tmp);
    }

    // Copy current style if it has changed since the previous glyph
    if (_invalidated_style || _glyphs.empty()) {
        _invalidated_style = false;
        new_glyph.style_changed = true;
        if (_css_font) {
            new_glyph.css_font = sp_repr_css_attr_new();
            sp_repr_css_merge(new_glyph.css_font, _css_font);
        }
    } else {
        new_glyph.style_changed = false;
        // Point to previous glyph's style information
        const SvgGlyph& prev_glyph = _glyphs.back();
        new_glyph.css_font = prev_glyph.css_font;
    }
    new_glyph.font_specification = _font_specification;
    new_glyph.rise = state->getRise();

    _glyphs.push_back(new_glyph);
}

/**
 * These text object functions are the outer most calls for begining and
 * ending text. No text functions should be called outside of these two calls
 */
void SvgBuilder::beginTextObject(GfxState *state) {
    _in_text_object = true;
    _invalidated_style = true;  // Force copying of current state
}

void SvgBuilder::endTextObject(GfxState *state)
{
    _in_text_object = false;
    _flushText(state);

    if (_clip_text_group) {
        // Use the clip as a real clip path
        _clip_text = _popContainer();
        _clip_text_group = nullptr;
    }
}

/**
 * Helper functions for supporting direct PNG output into a base64 encoded stream
 */
void png_write_vector(png_structp png_ptr, png_bytep data, png_size_t length)
{
    auto *v_ptr = reinterpret_cast<std::vector<guchar> *>(png_get_io_ptr(png_ptr)); // Get pointer to stream
    for ( unsigned i = 0 ; i < length ; i++ ) {
        v_ptr->push_back(data[i]);
    }
}

/**
 * \brief Creates an <image> element containing the given ImageStream as a PNG
 *
 */
Inkscape::XML::Node *SvgBuilder::_createImage(Stream *str, int width, int height,
                                              GfxImageColorMap *color_map, bool interpolate,
                                              int *mask_colors, bool alpha_only,
                                              bool invert_alpha) {

    // Create PNG write struct
    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    if ( png_ptr == nullptr ) {
        return nullptr;
    }
    // Create PNG info struct
    png_infop info_ptr = png_create_info_struct(png_ptr);
    if ( info_ptr == nullptr ) {
        png_destroy_write_struct(&png_ptr, nullptr);
        return nullptr;
    }
    // Set error handler
    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_write_struct(&png_ptr, &info_ptr);
        return nullptr;
    }
    // Decide whether we should embed this image
    bool embed_image = _preferences->getAttributeBoolean("embedImages", true);

    // Set read/write functions
    std::vector<guchar> png_buffer;
    FILE *fp = nullptr;
    gchar *file_name = nullptr;
    if (embed_image) {
        png_set_write_fn(png_ptr, &png_buffer, png_write_vector, nullptr);
    } else {
        static int counter = 0;
        file_name = g_strdup_printf("%s_img%d.png", _docname, counter++);
        fp = fopen(file_name, "wb");
        if ( fp == nullptr ) {
            png_destroy_write_struct(&png_ptr, &info_ptr);
            g_free(file_name);
            return nullptr;
        }
        png_init_io(png_ptr, fp);
    }

    // Set header data
    if ( !invert_alpha && !alpha_only ) {
        png_set_invert_alpha(png_ptr);
    }
    png_color_8 sig_bit;
    if (alpha_only) {
        png_set_IHDR(png_ptr, info_ptr,
                     width,
                     height,
                     8, /* bit_depth */
                     PNG_COLOR_TYPE_GRAY,
                     PNG_INTERLACE_NONE,
                     PNG_COMPRESSION_TYPE_BASE,
                     PNG_FILTER_TYPE_BASE);
        sig_bit.red = 0;
        sig_bit.green = 0;
        sig_bit.blue = 0;
        sig_bit.gray = 8;
        sig_bit.alpha = 0;
    } else {
        png_set_IHDR(png_ptr, info_ptr,
                     width,
                     height,
                     8, /* bit_depth */
                     PNG_COLOR_TYPE_RGB_ALPHA,
                     PNG_INTERLACE_NONE,
                     PNG_COMPRESSION_TYPE_BASE,
                     PNG_FILTER_TYPE_BASE);
        sig_bit.red = 8;
        sig_bit.green = 8;
        sig_bit.blue = 8;
        sig_bit.alpha = 8;
    }
    png_set_sBIT(png_ptr, info_ptr, &sig_bit);
    png_set_bgr(png_ptr);
    // Write the file header
    png_write_info(png_ptr, info_ptr);

    // Convert pixels
    ImageStream *image_stream;
    if (alpha_only) {
        if (color_map) {
            image_stream = new ImageStream(str, width, color_map->getNumPixelComps(),
                                           color_map->getBits());
        } else {
            image_stream = new ImageStream(str, width, 1, 1);
        }
        image_stream->reset();

        // Convert grayscale values
        unsigned char *buffer = new unsigned char[width];
        int invert_bit = invert_alpha ? 1 : 0;
        for ( int y = 0 ; y < height ; y++ ) {
            unsigned char *row = image_stream->getLine();
            if (color_map) {
                color_map->getGrayLine(row, buffer, width);
            } else {
                unsigned char *buf_ptr = buffer;
                for ( int x = 0 ; x < width ; x++ ) {
                    if ( row[x] ^ invert_bit ) {
                        *buf_ptr++ = 0;
                    } else {
                        *buf_ptr++ = 255;
                    }
                }
            }
            png_write_row(png_ptr, (png_bytep)buffer);
        }
        delete [] buffer;
    } else if (color_map) {
        image_stream = new ImageStream(str, width,
                                       color_map->getNumPixelComps(),
                                       color_map->getBits());
        image_stream->reset();

        // Convert RGB values
        unsigned int *buffer = new unsigned int[width];
        if (mask_colors) {
            for ( int y = 0 ; y < height ; y++ ) {
                unsigned char *row = image_stream->getLine();
                color_map->getRGBLine(row, buffer, width);

                unsigned int *dest = buffer;
                for ( int x = 0 ; x < width ; x++ ) {
                    // Check each color component against the mask
                    for ( int i = 0; i < color_map->getNumPixelComps() ; i++) {
                        if ( row[i] < mask_colors[2*i] * 255 ||
                             row[i] > mask_colors[2*i + 1] * 255 ) {
                            *dest = *dest | 0xff000000;
                            break;
                        }
                    }
                    // Advance to the next pixel
                    row += color_map->getNumPixelComps();
                    dest++;
                }
                // Write it to the PNG
                png_write_row(png_ptr, (png_bytep)buffer);
            }
        } else {
            for ( int i = 0 ; i < height ; i++ ) {
                unsigned char *row = image_stream->getLine();
                memset((void*)buffer, 0xff, sizeof(int) * width);
                color_map->getRGBLine(row, buffer, width);
                png_write_row(png_ptr, (png_bytep)buffer);
            }
        }
        delete [] buffer;

    } else {    // A colormap must be provided, so quit
        png_destroy_write_struct(&png_ptr, &info_ptr);
        if (!embed_image) {
            fclose(fp);
            g_free(file_name);
        }
        return nullptr;
    }
    delete image_stream;
    str->close();
    // Close PNG
    png_write_end(png_ptr, info_ptr);
    png_destroy_write_struct(&png_ptr, &info_ptr);

    // Create repr
    Inkscape::XML::Node *image_node = _xml_doc->createElement("svg:image");
    image_node->setAttributeSvgDouble("width", 1);
    image_node->setAttributeSvgDouble("height", 1);
    if( !interpolate ) {
        SPCSSAttr *css = sp_repr_css_attr_new();
        // This should be changed after CSS4 Images widely supported.
        sp_repr_css_set_property(css, "image-rendering", "optimizeSpeed");
        sp_repr_css_change(image_node, css, "style");
        sp_repr_css_attr_unref(css);
    }

    // PS/PDF images are placed via a transformation matrix, no preserveAspectRatio used
    image_node->setAttribute("preserveAspectRatio", "none");

    // Create href
    if (embed_image) {
        // Append format specification to the URI
        auto *base64String = g_base64_encode(png_buffer.data(), png_buffer.size());
        auto png_data = std::string("data:image/png;base64,") + base64String;
        g_free(base64String);
        image_node->setAttributeOrRemoveIfEmpty("xlink:href", png_data);
    } else {
        fclose(fp);
        image_node->setAttribute("xlink:href", file_name);
        g_free(file_name);
    }

    return image_node;
}

/**
 * \brief Creates a <mask> with the specified width and height and adds to <defs>
 *  If we're not the top-level SvgBuilder, creates a <defs> too and adds the mask to it.
 * \return the created XML node
 */
Inkscape::XML::Node *SvgBuilder::_createMask(double width, double height) {
    Inkscape::XML::Node *mask_node = _xml_doc->createElement("svg:mask");
    mask_node->setAttribute("maskUnits", "userSpaceOnUse");
    mask_node->setAttributeSvgDouble("x", 0.0);
    mask_node->setAttributeSvgDouble("y", 0.0);
    mask_node->setAttributeSvgDouble("width", width);
    mask_node->setAttributeSvgDouble("height", height);
    // Append mask to defs
    if (_is_top_level) {
        _doc->getDefs()->getRepr()->appendChild(mask_node);
        Inkscape::GC::release(mask_node);
        return _doc->getDefs()->getRepr()->lastChild();
    } else {    // Work around for renderer bug when mask isn't defined in pattern
        static int mask_count = 0;
        gchar *mask_id = g_strdup_printf("_mask%d", mask_count++);
        mask_node->setAttribute("id", mask_id);
        g_free(mask_id);
        _doc->getDefs()->getRepr()->appendChild(mask_node);
        Inkscape::GC::release(mask_node);
        return mask_node;
    }
}

void SvgBuilder::addImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map,
                          bool interpolate, int *mask_colors)
{
    Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, mask_colors);
    if (image_node) {
        _setBlendMode(image_node, state);
        _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
        _addToContainer(image_node);
        _setClipPath(image_node);
    }
}

void SvgBuilder::addImageMask(GfxState *state, Stream *str, int width, int height,
                              bool invert, bool interpolate) {

    // Create a rectangle
    Inkscape::XML::Node *rect = _addToContainer("svg:rect");
    rect->setAttributeSvgDouble("x", 0.0);
    rect->setAttributeSvgDouble("y", 0.0);
    rect->setAttributeSvgDouble("width", 1.0);
    rect->setAttributeSvgDouble("height", 1.0);

    // Get current fill style and set it on the rectangle
    SPCSSAttr *css = sp_repr_css_attr_new();
    _setFillStyle(css, state, false);
    sp_repr_css_change(rect, css, "style");
    sp_repr_css_attr_unref(css);
    _setBlendMode(rect, state);
    _setTransform(rect, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
    _setClipPath(rect);

    // Scaling 1x1 surfaces might not work so skip setting a mask with this size
    if ( width > 1 || height > 1 ) {
        Inkscape::XML::Node *mask_image_node =
            _createImage(str, width, height, nullptr, interpolate, nullptr, true, invert);
        if (mask_image_node) {
            // Create the mask
            Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0);
            // Remove unnecessary transformation from the mask image
            mask_image_node->removeAttribute("transform");
            mask_node->appendChild(mask_image_node);
            Inkscape::GC::release(mask_image_node);
            gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id"));
            rect->setAttribute("mask", mask_url);
            g_free(mask_url);
        }
    }
}

void SvgBuilder::addMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map,
                                bool interpolate, Stream *mask_str, int mask_width, int mask_height, bool invert_mask,
                                bool mask_interpolate)
{
    Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height,
                                          nullptr, mask_interpolate, nullptr, true, invert_mask);
    Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr);
    if ( mask_image_node && image_node ) {
        // Create mask for the image
        Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0);
        // Remove unnecessary transformation from the mask image
        mask_image_node->removeAttribute("transform");
        mask_node->appendChild(mask_image_node);
        // Scale the mask to the size of the image
        Geom::Affine mask_transform((double)width, 0.0, 0.0, (double)height, 0.0, 0.0);
        mask_node->setAttributeOrRemoveIfEmpty("maskTransform", sp_svg_transform_write(mask_transform));
        // Set mask and add image
        gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id"));
        image_node->setAttribute("mask", mask_url);
        g_free(mask_url);
        _setBlendMode(image_node, state);
        _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
        _addToContainer(image_node);
        _setClipPath(image_node);
    } else if (image_node) {
        Inkscape::GC::release(image_node);
    }
    if (mask_image_node) {
        Inkscape::GC::release(mask_image_node);
    }
}

void SvgBuilder::addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map,
                                    bool interpolate, Stream *mask_str, int mask_width, int mask_height,
                                    GfxImageColorMap *mask_color_map, bool mask_interpolate)
{
    Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height,
                                                        mask_color_map, mask_interpolate, nullptr, true);
    Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr);
    if ( mask_image_node && image_node ) {
        // Create mask for the image
        Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0);
        // Remove unnecessary transformation from the mask image
        mask_image_node->removeAttribute("transform");
        mask_node->appendChild(mask_image_node);
        // Set mask and add image
        gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id"));
        image_node->setAttribute("mask", mask_url);
        g_free(mask_url);
        _addToContainer(image_node);
        _setBlendMode(image_node, state);
        _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
        _setClipPath(image_node);
    } else if (image_node) {
        Inkscape::GC::release(image_node);
    }
    if (mask_image_node) {
        Inkscape::GC::release(mask_image_node);
    }
}

/**
 * Find the fill or stroke gradient we previously set on this node.
 */
Inkscape::XML::Node *SvgBuilder::_getGradientNode(Inkscape::XML::Node *node, bool is_fill)
{
    auto css = sp_repr_css_attr(node, "style");
    if (auto id = try_extract_uri_id(css->attribute(is_fill ? "fill" : "stroke"))) {
        if (auto obj = _doc->getObjectById(*id)) {
            return obj->getRepr();
        }
    }
    return nullptr;
}

bool SvgBuilder::_attrEqual(Inkscape::XML::Node *a, Inkscape::XML::Node *b, char const *attr)
{
    return (!a->attribute(attr) && !b->attribute(attr)) || std::string(a->attribute(attr)) == b->attribute(attr);
}

/**
 * Take a constructed mask and decide how to apply it to the target.
 */
void SvgBuilder::applyOptionalMask(Inkscape::XML::Node *mask, Inkscape::XML::Node *target)
{
    // Merge transparency gradient back into real gradient if possible
    if (mask->childCount() == 1) {
        auto source = mask->firstChild();
        auto source_gr = _getGradientNode(source, true);
        auto target_gr = _getGradientNode(target, true);
        // Both objects have a gradient, try and merge them
        if (source_gr && target_gr && source_gr->childCount() == target_gr->childCount()) {
            bool same_pos = _attrEqual(source_gr, target_gr, "x1") && _attrEqual(source_gr, target_gr, "x2")
                         && _attrEqual(source_gr, target_gr, "y1") && _attrEqual(source_gr, target_gr, "y2");

            bool white_mask = false;
            for (auto source_st = source_gr->firstChild(); source_st != nullptr; source_st = source_st->next()) {
                auto source_css = sp_repr_css_attr(source_st, "style");
                white_mask = white_mask or source_css->getAttributeDouble("stop-opacity") != 1.0;
                if (std::string(source_css->attribute("stop-color")) != "#ffffff") {
                    white_mask = false;
                    break;
                }
            }

            if (same_pos && white_mask) {
                // We move the stop-opacity from the source to the target
                auto target_st = target_gr->firstChild();
                for (auto source_st = source_gr->firstChild(); source_st != nullptr; source_st = source_st->next()) {
                    auto target_css = sp_repr_css_attr(target_st, "style");
                    auto source_css = sp_repr_css_attr(source_st, "style");
                    sp_repr_css_set_property(target_css, "stop-opacity", source_css->attribute("stop-opacity"));
                    sp_repr_css_change(target_st, target_css, "style");
                    target_st = target_st->next();
                }
                // Remove mask and gradient xml objects
                mask->parent()->removeChild(mask);
                source_gr->parent()->removeChild(source_gr);
                return;
            }
        }
    }
    gchar *mask_url = g_strdup_printf("url(#%s)", mask->attribute("id"));
    target->setAttribute("mask", mask_url);
    g_free(mask_url);
}


/**
 * \brief Starts building a new transparency group
 */
void SvgBuilder::startGroup(GfxState *state, double *bbox, GfxColorSpace * /*blending_color_space*/, bool isolated,
                           bool knockout, bool for_softmask)
{
    // Push group node, but don't attach to previous container yet
    _pushContainer("svg:g");

    if (for_softmask) {
        _mask_groups.push_back(state);
        // Create a container for the mask
        _pushContainer(_createMask(1.0, 1.0));
    }

    // TODO: In the future we could use state to insert transforms
    // and then remove the inverse from the items added into the children
    // to reduce the transformational duplication.
}

void SvgBuilder::finishGroup(GfxState *state, bool for_softmask)
{
    if (for_softmask) {
        // Create mask
        auto mask_node = _popContainer();
        applyOptionalMask(mask_node, _container);
    } else {
        popGroup(state);
    }
}

void SvgBuilder::popGroup(GfxState *state)
{
    // Restore node stack
    auto parent = _popContainer();
    bool will_clip = _clip_history->hasClipPath() && !_clip_history->isBoundingBox();

    if (parent->childCount() == 1 && !parent->attribute("transform")) {
        // Merge this opacity and remove unnecessary group
        auto child = parent->firstChild();

        if (will_clip && child->attribute("d")) {
            // Note to future: this means the group contains a single path, this path is likely
            // a fake bounding box path and the real path is contained within the clipping region
            // Moving the clipping region out into the path object and deleting the group would
            // improve output here.
        }

        // Do not merge masked or clipped groups, to avoid clobering
        if (!will_clip && !child->attribute("mask") && !child->attribute("clip-path")) {
            auto orig = child->getAttributeDouble("opacity", 1.0);
            auto grp = parent->getAttributeDouble("opacity", 1.0);
            child->setAttributeSvgDouble("opacity", orig * grp);

            if (auto mask_id = try_extract_uri_id(parent->attribute("mask"))) {
                if (auto obj = _doc->getObjectById(*mask_id)) {
                    applyOptionalMask(obj->getRepr(), child);
                }
            }
            if (auto clip = parent->attribute("clip-path")) {
                child->setAttribute("clip-path", clip);
            }

            // This duplicate child will get applied in the place of the group
            parent->removeChild(child);
            Inkscape::GC::anchor(child);
            parent = child;
        }
    }

    // Add the parent to the last container
    _addToContainer(parent);
    _setClipPath(parent);
}

/**
 * Decide what to do for each font in the font list, with the given strategy.
 */
FontStrategies SvgBuilder::autoFontStrategies(FontStrategy s, FontList fonts)
{
    FontStrategies ret;
    for (auto font : *fonts.get()) {
        int id = font.first->getID()->num;
        bool found = font.second.found;
        switch (s) {
            case FontStrategy::RENDER_ALL:
                ret[id] = FontFallback::AS_SHAPES;
                break;
            case FontStrategy::DELETE_ALL:
                ret[id] = FontFallback::DELETE_TEXT;
                break;
            case FontStrategy::RENDER_MISSING:
                ret[id] = found ? FontFallback::AS_TEXT : FontFallback::AS_SHAPES;
                break;
            case FontStrategy::SUBSTITUTE_MISSING:
                ret[id] = found ? FontFallback::AS_TEXT : FontFallback::AS_SUB;
                break;
            case FontStrategy::KEEP_MISSING:
                ret[id] = FontFallback::AS_TEXT;
                break;
            case FontStrategy::DELETE_MISSING:
                ret[id] = found ? FontFallback::AS_TEXT : FontFallback::DELETE_TEXT;
                break;
        }
    }
    return ret;
}
} } } /* namespace Inkscape, Extension, Internal */

#endif /* HAVE_POPPLER */

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
