// SPDX-License-Identifier: GPL-2.0-or-later
/** \file
 * Rendering with Cairo.
 */
/*
 * Author:
 *   Miklos Erdelyi <erdelyim@gmail.com>
 *   Jon A. Cruz <jon@joncruz.org>
 *   Abhishek Sharma
 *
 * Copyright (C) 2006 Miklos Erdelyi
 *
 * 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

#ifndef PANGO_ENABLE_BACKEND
#define PANGO_ENABLE_BACKEND
#endif

#ifndef PANGO_ENABLE_ENGINE
#define PANGO_ENABLE_ENGINE
#endif


#include <csignal>
#include <cerrno>


#include <2geom/transforms.h>
#include <2geom/pathvector.h>
#include <cairo.h>
#include <glib.h>
#include <glibmm/i18n.h>

// include support for only the compiled-in surface types
#ifdef CAIRO_HAS_PDF_SURFACE
#include <cairo-pdf.h>
#endif
#ifdef CAIRO_HAS_PS_SURFACE
#include <cairo-ps.h>
#endif

#include "cairo-render-context.h"
#include "cairo-renderer.h"
#include "document.h"
#include "inkscape-version.h"
#include "rdf.h"
#include "style-internal.h"
#include "display/cairo-utils.h"
#include "display/curve.h"
#include "extension/system.h"
#include "filter-chemistry.h"
#include "helper/pixbuf-ops.h"
#include "helper/png-write.h"

#include "io/sys.h"

#include "include/source_date_epoch.h"

#include "libnrtype/Layout-TNG.h"

#include "object/sp-anchor.h"
#include "object/sp-clippath.h"
#include "object/sp-defs.h"
#include "object/sp-flowtext.h"
#include "object/sp-hatch-path.h"
#include "object/sp-image.h"
#include "object/sp-item-group.h"
#include "object/sp-item.h"
#include "object/sp-linear-gradient.h"
#include "object/sp-marker.h"
#include "object/sp-mask.h"
#include "object/sp-page.h"
#include "object/sp-pattern.h"
#include "object/sp-radial-gradient.h"
#include "object/sp-root.h"
#include "object/sp-shape.h"
#include "object/sp-symbol.h"
#include "object/sp-text.h"
#include "object/sp-use.h"

#include "util/units.h"

//#define TRACE(_args) g_printf _args
#define TRACE(_args)
//#define TEST(_args) _args
#define TEST(_args)

namespace Inkscape {
namespace Extension {
namespace Internal {

CairoRenderer::CairoRenderer(void)
= default;

CairoRenderer::~CairoRenderer()
{
    /* restore default signal handling for SIGPIPE */
#if !defined(_WIN32) && !defined(__WIN32__)
    (void) signal(SIGPIPE, SIG_DFL);
#endif

    return;
}

CairoRenderContext*
CairoRenderer::createContext()
{
    CairoRenderContext *new_context = new CairoRenderContext(this);
    g_assert( new_context != nullptr );

    new_context->_state = nullptr;

    // create initial render state
    CairoRenderState *state = new_context->_createState();
    state->transform = Geom::identity();
    new_context->_state_stack.push_back(state);
    new_context->_state = state;

    return new_context;
}

void
CairoRenderer::destroyContext(CairoRenderContext *ctx)
{
    delete ctx;
}

/*

Here comes the rendering part which could be put into the 'render' methods of SPItems'

*/

/* The below functions are copy&pasted plus slightly modified from *_invoke_print functions. */
static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx, SPItem *origin = nullptr, SPPage *page = nullptr);
static void sp_group_render(SPGroup *group, CairoRenderContext *ctx, SPItem *origin = nullptr, SPPage *page = nullptr);
static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx);
static void sp_use_render(SPUse *use, CairoRenderContext *ctx, SPPage *page = nullptr);
static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx, SPItem *origin = nullptr);
static void sp_text_render(SPText *text, CairoRenderContext *ctx);
static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx);
static void sp_image_render(SPImage *image, CairoRenderContext *ctx);
static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx, SPItem *origin, SPPage *page);
static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx, SPPage *page = nullptr);

static void sp_shape_render_invoke_marker_rendering(SPMarker* marker, Geom::Affine tr, SPStyle* style, CairoRenderContext *ctx, SPItem *origin)
{
    bool render = true;
    if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) {
        if (style->stroke_width.computed > 1e-9) {
            tr = Geom::Scale(style->stroke_width.computed) * tr;
        } else {
            render = false; // stroke width zero and marker is thus scaled down to zero, skip
        }
    }

    if (render) {
        SPItem* marker_item = sp_item_first_item_child(marker);
        if (marker_item) {
            tr = (Geom::Affine)marker_item->transform * (Geom::Affine)marker->c2p * tr;
            Geom::Affine old_tr = marker_item->transform;
            marker_item->transform = tr;
            ctx->getRenderer()->renderItem (ctx, marker_item, origin);
            marker_item->transform = old_tr;
        }
    }
}

static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx, SPItem *origin)
{
    if (!shape->curve()) {
        return;
    }

    Geom::OptRect pbox = shape->geometricBounds();
    
    SPStyle* style = shape->style;
    auto fill_origin = style->fill.paintOrigin;
    auto stroke_origin = style->stroke.paintOrigin;

    if (origin) {
        // If the shape is a child of a marker, we must set styles from the origin.
        SPMarker *marker = nullptr;
        auto parentobj = shape->parent;
        while (parentobj) {
            if (marker = dynamic_cast<SPMarker *>(parentobj)) {
                break;
            }
            parentobj = parentobj->parent;
        }
        if (marker) {
            // Origin will ultimately be either the original object the marker
            // was added to OR a use tag. See sp_use_render for where this object
            // comes from when it's a clone.
            SPStyle *styleorig   = origin->style;
            bool iscolorfill     = styleorig->fill.isColor() || (styleorig->fill.isPaintserver() && !styleorig->getFillPaintServer()->isValid());
            bool iscolorstroke   = styleorig->stroke.isColor() || (styleorig->stroke.isPaintserver() && !styleorig->getStrokePaintServer()->isValid());
            bool fillctxfill     = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL;
            bool fillctxstroke   = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE;
            bool strokectxfill   = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL;
            bool strokectxstroke = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE;

            if (fillctxfill || fillctxstroke) {
                if (fillctxfill ? iscolorfill : iscolorstroke) {
                    style->fill.setColor(fillctxfill ? styleorig->fill.value.color : styleorig->stroke.value.color);
                } else if (fillctxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) {
                    style->fill.value.href = fillctxfill ? styleorig->fill.value.href : styleorig->stroke.value.href;
                } else {
                    style->fill.setNone();
                }
            }
            if (strokectxfill || strokectxstroke) {
                if (strokectxfill ? iscolorfill : iscolorstroke) {
                    style->stroke.setColor(strokectxfill ? styleorig->fill.value.color : styleorig->stroke.value.color);
                } else if (strokectxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) {
                    style->stroke.value.href = strokectxfill ? styleorig->fill.value.href : styleorig->stroke.value.href;
                } else {
                    style->stroke.setNone();
                }
            }
            style->fill.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL;
            style->stroke.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL;
        }
    }

    Geom::PathVector const &pathv = shape->curve()->get_pathvector();
    if (pathv.empty()) {
        return;
    }

    if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_NORMAL ||
        (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL &&
         style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE)) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL);
    } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE &&
               style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL ) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE);
    } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE &&
               style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY);
    } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL &&
               style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY);
    }

    // START marker
    for (int i = 0; i < 2; i++) {  // SP_MARKER_LOC and SP_MARKER_LOC_START
        if ( shape->_marker[i] ) {
            SPMarker* marker = shape->_marker[i];
            Geom::Affine tr;
            if (marker->orient_mode == MARKER_ORIENT_AUTO) {
                tr = sp_shape_marker_get_transform_at_start(pathv.begin()->front());
            } else if (marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) {
                tr = Geom::Rotate::from_degrees( 180.0 ) * sp_shape_marker_get_transform_at_start(pathv.begin()->front());
            } else {
                tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(pathv.begin()->front().pointAt(0));
            }
            sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape);
        }
    }
    // MID marker
    for (int i = 0; i < 3; i += 2) {  // SP_MARKER_LOC and SP_MARKER_LOC_MID
        if ( !shape->_marker[i] ) continue;
        SPMarker* marker = shape->_marker[i];
        for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) {
            // START position
            if ( path_it != pathv.begin()
                 && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there
            {
                Geom::Affine tr;
                if (marker->orient_mode != MARKER_ORIENT_ANGLE) {
                    tr = sp_shape_marker_get_transform_at_start(path_it->front());
                } else {
                    tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(path_it->front().pointAt(0));
                }
                sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape);
            }
            // MID position
            if (path_it->size_default() > 1) {
                Geom::Path::const_iterator curve_it1 = path_it->begin();      // incoming curve
                Geom::Path::const_iterator curve_it2 = ++(path_it->begin());  // outgoing curve
                while (curve_it2 != path_it->end_default())
                {
                    /* Put marker between curve_it1 and curve_it2.
                     * Loop to end_default (so including closing segment), because when a path is closed,
                     * there should be a midpoint marker between last segment and closing straight line segment */
                    Geom::Affine tr;
                    if (marker->orient_mode != MARKER_ORIENT_ANGLE) {
                        tr = sp_shape_marker_get_transform(*curve_it1, *curve_it2);
                    } else {
                        tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(curve_it1->pointAt(1));
                    }

                    sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape);

                    ++curve_it1;
                    ++curve_it2;
                }
            }
            // END position
            if ( path_it != (pathv.end()-1) && !path_it->empty()) {
                Geom::Curve const &lastcurve = path_it->back_default();
                Geom::Affine tr;
                if (marker->orient_mode != MARKER_ORIENT_ANGLE) {
                    tr = sp_shape_marker_get_transform_at_end(lastcurve);
                } else {
                    tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1));
                }
                sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape);
            }
        }
    }
    // END marker
    for (int i = 0; i < 4; i += 3) {  // SP_MARKER_LOC and SP_MARKER_LOC_END
        if ( shape->_marker[i] ) {
            SPMarker* marker = shape->_marker[i];

            /* Get reference to last curve in the path.
             * For moveto-only path, this returns the "closing line segment". */
            Geom::Path const &path_last = pathv.back();
            unsigned int index = path_last.size_default();
            if (index > 0) {
                index--;
            }
            Geom::Curve const &lastcurve = path_last[index];

            Geom::Affine tr;
            if (marker->orient_mode != MARKER_ORIENT_ANGLE) {
                tr = sp_shape_marker_get_transform_at_end(lastcurve);
            } else {
                tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1));
            }

            sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape);
        }
    }

    if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL &&
        style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL);
    } else if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE &&
               style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL ) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE);
    } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE &&
               style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY);
    } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL &&
               style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) {
        ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY);
    }

    // Put the style's paint origin back, in case this shape is a marker
    // which is rendered multple times.
    style->fill.paintOrigin = fill_origin;
    style->stroke.paintOrigin = stroke_origin;
}

static void sp_group_render(SPGroup *group, CairoRenderContext *ctx, SPItem *origin, SPPage *page)
{
    CairoRenderer *renderer = ctx->getRenderer();
    for (auto obj : group->childList(false)) {
        if (SPItem *item = dynamic_cast<SPItem *>(obj)) {
            renderer->renderItem(ctx, item, origin, page);
        }
    }
}

static void sp_use_render(SPUse *use, CairoRenderContext *ctx, SPPage *page)
{
    bool translated = false;
    CairoRenderer *renderer = ctx->getRenderer();

    if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) {
        // FIXME: This translation sometimes isn't in the correct units; e.g.
        // x="0" y="42" has a different effect than transform="translate(0,42)".
        Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed));
        ctx->pushState();
        ctx->transform(tp);
        translated = true;
    }

    if (use->child) {
        // Padding in the use object as the origin here ensures markers
        // are rendered with their correct context-fill.
        renderer->renderItem(ctx, use->child, use, page);
    }

    if (translated) {
        ctx->popState();
    }
}

static void sp_text_render(SPText *text, CairoRenderContext *ctx)
{
    text->layout.showGlyphs(ctx);
}

static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx)
{
    flowtext->layout.showGlyphs(ctx);
}

static void sp_image_render(SPImage *image, CairoRenderContext *ctx)
{
    if (!image->pixbuf) {
        return;
    }
    if ((image->width.computed <= 0.0) || (image->height.computed <= 0.0)) {
        return;
    }

    int w = image->pixbuf->width();
    int h = image->pixbuf->height();

    double x = image->x.computed;
    double y = image->y.computed;
    double width = image->width.computed;
    double height = image->height.computed;

    if (image->aspect_align != SP_ASPECT_NONE) {
        calculatePreserveAspectRatio (image->aspect_align, image->aspect_clip, (double)w, (double)h,
                                                     &x, &y, &width, &height);
    }

    if (image->aspect_clip == SP_ASPECT_SLICE && !ctx->getCurrentState()->has_overflow) {
        ctx->addClippingRect(image->x.computed, image->y.computed, image->width.computed, image->height.computed);
    }

    Geom::Translate tp(x, y);
    Geom::Scale s(width / (double)w, height / (double)h);
    Geom::Affine t(s * tp);

    ctx->renderImage(image->pixbuf, t, image->style);
}

static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx)
{
    CairoRenderer *renderer = ctx->getRenderer();

    std::vector<SPObject*> l(a->childList(false));
    if (a->href)
        ctx->tagBegin(a->href);
    for(auto x : l){
        SPItem *item = dynamic_cast<SPItem*>(x);
        if (item) {
            renderer->renderItem(ctx, item);
        }
    }
    if (a->href)
        ctx->tagEnd();
}

static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx, SPItem *origin, SPPage *page)
{
    if (!symbol->cloned) {
        return;
    }

    /* Cloned <symbol> is actually renderable */
    ctx->pushState();
    ctx->transform(symbol->c2p);

    // apply viewbox if set
    if (false /*symbol->viewBox_set*/) {
        Geom::Affine vb2user;
        double x, y, width, height;
        double view_width, view_height;
        x = 0.0;
        y = 0.0;
        width = 1.0;
        height = 1.0;

        view_width = symbol->viewBox.width();
        view_height = symbol->viewBox.height();

        calculatePreserveAspectRatio(symbol->aspect_align, symbol->aspect_clip, view_width, view_height,
                                     &x, &y,&width, &height);

        // [itemTransform *] translate(x, y) * scale(w/vw, h/vh) * translate(-vx, -vy);
        vb2user = Geom::identity();
        vb2user[0] = width / view_width;
        vb2user[3] = height / view_height;
        vb2user[4] = x - symbol->viewBox.left() * vb2user[0];
        vb2user[5] = y - symbol->viewBox.top() * vb2user[3];

        ctx->transform(vb2user);
    }

    sp_group_render(symbol, ctx, origin, page);
    ctx->popState();
}

static void sp_root_render(SPRoot *root, CairoRenderContext *ctx)
{
    CairoRenderer *renderer = ctx->getRenderer();

    if (!ctx->getCurrentState()->has_overflow && root->parent)
        ctx->addClippingRect(root->x.computed, root->y.computed, root->width.computed, root->height.computed);

    ctx->pushState();
    renderer->setStateForItem(ctx, root);
    ctx->transform(root->c2p);
    sp_group_render(root, ctx);
    ctx->popState();
}

/**
    This function converts the item to a raster image and includes the image into the cairo renderer.
    It is only used for filters and then only when rendering filters as bitmaps is requested.
*/
static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx, SPPage *page)
{

    // The code was adapted from sp_selection_create_bitmap_copy in selection-chemistry.cpp

    // Calculate resolution
    double res;
    /** @TODO reimplement the resolution stuff   (WHY?)
    */
    res = ctx->getBitmapResolution();
    if(res == 0) {
        res = Inkscape::Util::Quantity::convert(1, "in", "px");
    }
    TRACE(("sp_asbitmap_render: resolution: %f\n", res ));

    // Get the bounding box of the selection in document coordinates.
    Geom::OptRect bbox = item->documentVisualBounds();

    bbox &= (page ? page->getDocumentRect() : item->document->preferredBounds());

    // no bbox, e.g. empty group or item not overlapping its page
    if (!bbox) {
        return;
    }

    // The width and height of the bitmap in pixels
    unsigned width =  ceil(bbox->width() * Inkscape::Util::Quantity::convert(res, "px", "in"));
    unsigned height = ceil(bbox->height() * Inkscape::Util::Quantity::convert(res, "px", "in"));

    if (width == 0 || height == 0) return;

    // Scale to exactly fit integer bitmap inside bounding box
    double scale_x = bbox->width() / width;
    double scale_y = bbox->height() / height;

    // Location of bounding box in document coordinates.
    double shift_x = bbox->min()[Geom::X];
    double shift_y = bbox->top();

    // For default 96 dpi, snap bitmap to pixel grid
    if (res == Inkscape::Util::Quantity::convert(1, "in", "px")) {
        shift_x = round (shift_x);
        shift_y = round (shift_y);
    }

    // Calculate the matrix that will be applied to the image so that it exactly overlaps the source objects

    // Matrix to put bitmap in correct place on document
    Geom::Affine t_on_document = (Geom::Affine)(Geom::Scale (scale_x, scale_y)) *
                                 (Geom::Affine)(Geom::Translate (shift_x, shift_y));

    // ctx matrix already includes item transformation. We must substract.
    Geom::Affine t_item =  item->i2doc_affine();
    Geom::Affine t = t_on_document * t_item.inverse();

    // Do the export
    SPDocument *document = item->document;

    std::vector<SPItem*> items;
    items.push_back(item);

    std::unique_ptr<Inkscape::Pixbuf> pb(sp_generate_internal_bitmap(document, *bbox, res, items, true));

    if (pb) {
        //TEST(gdk_pixbuf_save( pb, "bitmap.png", "png", NULL, NULL ));

        ctx->renderImage(pb.get(), t, item->style);
    }
}


static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx, SPItem *origin, SPPage *page)
{
    SPRoot *root = dynamic_cast<SPRoot *>(item);
    if (root) {
        TRACE(("root\n"));
        sp_root_render(root, ctx);
    } else {
        SPSymbol *symbol = dynamic_cast<SPSymbol *>(item);
        if (symbol) {
            TRACE(("symbol\n"));
            sp_symbol_render(symbol, ctx, origin, page);
        } else {
            SPAnchor *anchor = dynamic_cast<SPAnchor *>(item);
            if (anchor) {
                TRACE(("<a>\n"));
                sp_anchor_render(anchor, ctx);
            } else {
                SPShape *shape = dynamic_cast<SPShape *>(item);
                if (shape) {
                    TRACE(("shape\n"));
                    sp_shape_render(shape, ctx, origin);
                } else {
                    SPUse *use = dynamic_cast<SPUse *>(item);
                    if (use) {
                        TRACE(("use begin---\n"));
                        sp_use_render(use, ctx, page);
                        TRACE(("---use end\n"));
                    } else {
                        SPText *text = dynamic_cast<SPText *>(item);
                        if (text) {
                            TRACE(("text\n"));
                            sp_text_render(text, ctx);
                        } else {
                            SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item);
                            if (flowtext) {
                                TRACE(("flowtext\n"));
                                sp_flowtext_render(flowtext, ctx);
                            } else {
                                SPImage *image = dynamic_cast<SPImage *>(item);
                                if (image) {
                                    TRACE(("image\n"));
                                    sp_image_render(image, ctx);
                                } else if (dynamic_cast<SPMarker *>(item)) {
                                    // Marker contents shouldn't be rendered, even outside of <defs>.
                                    return;
                                } else {
                                    SPGroup *group = dynamic_cast<SPGroup *>(item);
                                    if (group) {
                                        TRACE(("<g>\n"));
                                        sp_group_render(group, ctx, origin, page);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

void
CairoRenderer::setStateForItem(CairoRenderContext *ctx, SPItem const *item)
{
    ctx->setStateForStyle(item->style);

    CairoRenderState *state = ctx->getCurrentState();
    state->clip_path = item->getClipObject();
    state->mask = item->getMaskObject();
    state->item_transform = Geom::Affine (item->transform);

    // If parent_has_userspace is true the parent state's transform
    // has to be used for the mask's/clippath's context.
    // This is so because we use the image's/(flow)text's transform for positioning
    // instead of explicitly specifying it and letting the renderer do the
    // transformation before rendering the item.
    if (dynamic_cast<SPText const *>(item) || dynamic_cast<SPFlowtext const *>(item) || dynamic_cast<SPImage const *>(item)) {
        state->parent_has_userspace = TRUE;
    }
    TRACE(("setStateForItem opacity: %f\n", state->opacity));
}

bool CairoRenderer::_shouldRasterize(CairoRenderContext *ctx, SPItem const *item)
{
    // rasterize filtered items as per user setting
    // however, clipPaths ignore any filters, so do *not* rasterize
    // TODO: might apply to some degree to masks with filtered elements as well;
    //       we need to figure out where in the stack it would be safe to rasterize
    if (ctx->getFilterToBitmap() && !item->isInClipPath()) {
        if (auto const *clone = dynamic_cast<SPUse const *>(item)) {
            return clone->anyInChain([](SPItem const *i) { return i && i->isFiltered(); });
        } else {
            return item->isFiltered();
        }
    }
    return false;
}

void CairoRenderer::_doRender(SPItem *item, CairoRenderContext *ctx, SPItem *origin, SPPage *page)
{
    // Check item's visibility
    if (item->isHidden() || has_hidder_filter(item)) {
        return;
    }

    if (_shouldRasterize(ctx, item)) {
        sp_asbitmap_render(item, ctx, page);
    } else {
        sp_item_invoke_render(item, ctx, origin, page);
    }
}

// TODO change this to accept a const SPItem:
void CairoRenderer::renderItem(CairoRenderContext *ctx, SPItem *item, SPItem *origin, SPPage *page)
{
    ctx->pushState();
    setStateForItem(ctx, item);

    CairoRenderState *state = ctx->getCurrentState();
    state->need_layer = ( state->mask || state->clip_path || state->opacity != 1.0 );
    SPStyle* style = item->style;
    SPGroup * group = dynamic_cast<SPGroup *>(item);
    bool blend = false;
    if (group && style->mix_blend_mode.set && style->mix_blend_mode.value != SP_CSS_BLEND_NORMAL) {
        state->need_layer = true;
        blend = true;
    } 
    // Draw item on a temporary surface so a mask, clip-path, or opacity can be applied to it.
    if (state->need_layer) {
        state->merge_opacity = FALSE;
        ctx->pushLayer();
    }

    ctx->transform(item->transform);

    _doRender(item, ctx, origin, page);

    if (state->need_layer) {
        if (blend) {
            ctx->popLayer(ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); // This applies clipping/masking
        } else {
            ctx->popLayer(); // This applies clipping/masking
        }
    }
    ctx->popState();
}

void CairoRenderer::renderHatchPath(CairoRenderContext *ctx, SPHatchPath const &hatchPath, unsigned key) {
    ctx->pushState();
    ctx->setStateForStyle(hatchPath.style);
    ctx->transform(Geom::Translate(hatchPath.offset.computed, 0));

    std::unique_ptr<SPCurve> curve = hatchPath.calculateRenderCurve(key);
    Geom::PathVector const & pathv =curve->get_pathvector();
    if (!pathv.empty()) {
        ctx->renderPathVector(pathv, hatchPath.style, Geom::OptRect());
    }

    ctx->popState();
}

void CairoRenderer::setMetadata(CairoRenderContext *ctx, SPDocument *doc) {
    // title
    const gchar *title = rdf_get_work_entity(doc, rdf_find_entity("title"));
    if (title) {
        ctx->_metadata.title = title;
    }

    // author
    const gchar *author = rdf_get_work_entity(doc, rdf_find_entity("creator"));
    if (author) {
        ctx->_metadata.author = author;
    }

    // subject
    const gchar *subject = rdf_get_work_entity(doc, rdf_find_entity("description"));
    if (subject) {
        ctx->_metadata.subject = subject;
    }

    // keywords
    const gchar *keywords = rdf_get_work_entity(doc, rdf_find_entity("subject"));
    if (keywords) {
        ctx->_metadata.keywords = keywords;
    }

    // copyright
    const gchar *copyright = rdf_get_work_entity(doc, rdf_find_entity("rights"));
    if (copyright) {
        ctx->_metadata.copyright = copyright;
    }

    // creator
    ctx->_metadata.creator = Glib::ustring::compose("Inkscape %1 (https://inkscape.org)",
                                                    Inkscape::version_string_without_revision);

    // cdate (only used for for reproducible builds hack)
    Glib::ustring cdate = ReproducibleBuilds::now_iso_8601();
    if (!cdate.empty()) {
        ctx->_metadata.cdate = cdate;
    }

    // mdate (currently unused)
}

bool
CairoRenderer::setupDocument(CairoRenderContext *ctx, SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base)
{
// PLEASE note when making changes to the boundingbox and transform calculation, corresponding changes should be made to LaTeXTextRenderer::setupDocument !!!

    g_assert( ctx != nullptr );

    if (!base) {
        base = doc->getRoot();
    }

    Geom::Rect d;
    if (pageBoundingBox) {
        d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions());
    } else {
        Geom::OptRect bbox = base->documentVisualBounds();
        if (!bbox) {
            g_message("CairoRenderer: empty bounding box.");
            return false;
        }
        d = *bbox;
    }
    d.expandBy(bleedmargin_px);

    double px_to_ctx_units = 1.0;
    if (ctx->_vector_based_target) {
        // convert from px to pt
        px_to_ctx_units = Inkscape::Util::Quantity::convert(1, "px", "pt");
    }

    auto width = d.width() * px_to_ctx_units;
    auto height = d.height() * px_to_ctx_units;

    setMetadata(ctx, doc);

    TRACE(("setupDocument: %f x %f\n", width, height));
    bool ret = ctx->setupSurface(width, height);

    if (ret) {
        if (pageBoundingBox) {
            // translate to set bleed/margin
            Geom::Affine tp( Geom::Translate( bleedmargin_px, bleedmargin_px ) );
            ctx->transform(tp);
        } else {
            // this transform translates the export drawing to a virtual page (0,0)-(width,height)
            Geom::Affine tp(Geom::Translate(-d.min()));
            ctx->transform(tp);
        }
    }

    return ret;
}

// Apply an SVG clip path
void
CairoRenderer::applyClipPath(CairoRenderContext *ctx, SPClipPath const *cp)
{
    g_assert( ctx != nullptr && ctx->_is_valid );

    if (cp == nullptr)
        return;

    CairoRenderContext::CairoRenderMode saved_mode = ctx->getRenderMode();
    ctx->setRenderMode(CairoRenderContext::RENDER_MODE_CLIP);

    // FIXME: the access to the first clippath view to obtain the bbox is completely bogus
    Geom::Affine saved_ctm;
    if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && cp->display->bbox) {
        Geom::Rect clip_bbox = *cp->display->bbox;
        Geom::Affine t(Geom::Scale(clip_bbox.dimensions()));
        t[4] = clip_bbox.left();
        t[5] = clip_bbox.top();
        t *= ctx->getCurrentState()->transform;
        saved_ctm = ctx->getTransform();
        ctx->setTransform(t);
    }

    TRACE(("BEGIN clip\n"));
    SPObject const *co = cp;
    for (auto& child: co->children) {
        SPItem const *item = dynamic_cast<SPItem const *>(&child);
        if (item) {

            // combine transform of the item in clippath and the item using clippath:
            Geom::Affine tempmat = item->transform * ctx->getCurrentState()->item_transform;

            // render this item in clippath
            ctx->pushState();
            ctx->transform(tempmat);
            setStateForItem(ctx, item);
            // TODO fix this call to accept const items
            _doRender(const_cast<SPItem *>(item), ctx);
            ctx->popState();
        }
    }
    TRACE(("END clip\n"));

    // do clipping only if this was the first call to applyClipPath
    if (ctx->getClipMode() == CairoRenderContext::CLIP_MODE_PATH
        && saved_mode == CairoRenderContext::RENDER_MODE_NORMAL)
        cairo_clip(ctx->_cr);

    if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX)
        ctx->setTransform(saved_ctm);

    ctx->setRenderMode(saved_mode);
}

// Apply an SVG mask
void
CairoRenderer::applyMask(CairoRenderContext *ctx, SPMask const *mask)
{
    g_assert( ctx != nullptr && ctx->_is_valid );

    if (mask == nullptr)
        return;

    // FIXME: the access to the first mask view to obtain the bbox is completely bogus
    // TODO: should the bbox be transformed if maskUnits != userSpaceOnUse ?
    if (mask->maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && mask->display->bbox) {
        Geom::Rect mask_bbox = *mask->display->bbox;
        Geom::Affine t(Geom::Scale(mask_bbox.dimensions()));
        t[4] = mask_bbox.left();
        t[5] = mask_bbox.top();
        t *= ctx->getCurrentState()->transform;
        ctx->setTransform(t);
    }

    // Clip mask contents... but...
    // The mask's bounding box is the "geometric bounding box" which doesn't allow for
    // filters which extend outside the bounding box. So don't clip.
    // ctx->addClippingRect(mask_bbox.x0, mask_bbox.y0, mask_bbox.x1 - mask_bbox.x0, mask_bbox.y1 - mask_bbox.y0);

    ctx->pushState();

    TRACE(("BEGIN mask\n"));
    SPObject const *co = mask;
    for (auto& child: co->children) {
        SPItem const *item = dynamic_cast<SPItem const *>(&child);
        if (item) {
            // TODO fix const correctness:
            renderItem(ctx, const_cast<SPItem*>(item));
        }
    }
    TRACE(("END mask\n"));

    ctx->popState();
}

void
calculatePreserveAspectRatio(unsigned int aspect_align, unsigned int aspect_clip, double vp_width, double vp_height,
                             double *x, double *y, double *width, double *height)
{
    if (aspect_align == SP_ASPECT_NONE)
        return;

    double scalex, scaley, scale;
    double new_width, new_height;
    scalex = *width / vp_width;
    scaley = *height / vp_height;
    scale = (aspect_clip == SP_ASPECT_MEET) ? MIN(scalex, scaley) : MAX(scalex, scaley);
    new_width = vp_width * scale;
    new_height = vp_height * scale;
    /* Now place viewbox to requested position */
    switch (aspect_align) {
        case SP_ASPECT_XMIN_YMIN:
            break;
        case SP_ASPECT_XMID_YMIN:
            *x -= 0.5 * (new_width - *width);
            break;
        case SP_ASPECT_XMAX_YMIN:
            *x -= 1.0 * (new_width - *width);
            break;
        case SP_ASPECT_XMIN_YMID:
            *y -= 0.5 * (new_height - *height);
            break;
        case SP_ASPECT_XMID_YMID:
            *x -= 0.5 * (new_width - *width);
            *y -= 0.5 * (new_height - *height);
            break;
        case SP_ASPECT_XMAX_YMID:
            *x -= 1.0 * (new_width - *width);
            *y -= 0.5 * (new_height - *height);
            break;
        case SP_ASPECT_XMIN_YMAX:
            *y -= 1.0 * (new_height - *height);
            break;
        case SP_ASPECT_XMID_YMAX:
            *x -= 0.5 * (new_width - *width);
            *y -= 1.0 * (new_height - *height);
            break;
        case SP_ASPECT_XMAX_YMAX:
            *x -= 1.0 * (new_width - *width);
            *y -= 1.0 * (new_height - *height);
            break;
        default:
            break;
    }
    *width = new_width;
    *height = new_height;
}

#include "clear-n_.h"

}  /* namespace Internal */
}  /* namespace Extension */
}  /* namespace Inkscape */

#undef TRACE


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