From c853ffb5b2f75f5a889ed2e3ef89b818a736e87a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:50:49 +0200 Subject: Adding upstream version 1.3+ds. Signed-off-by: Daniel Baumann --- src/extension/internal/pdfinput/svg-builder.cpp | 2311 +++++++++++++++++++++++ 1 file changed, 2311 insertions(+) create mode 100644 src/extension/internal/pdfinput/svg-builder.cpp (limited to 'src/extension/internal/pdfinput/svg-builder.cpp') diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp new file mode 100644 index 0000000..fbab2ff --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -0,0 +1,2311 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Jon A. Cruz + * + * 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 +#include +#include + +#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(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 &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(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(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(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(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(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(shading))->getColor(offset, result); + } else if ( shading->getType() == 3 ) { // Radial shading + (static_cast(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 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->getFamily()) { // if font family is explicitly given use it. + sp_repr_css_set_property(_css_font, "font-family", font->getFamily()->getCString()); + } else if (font_strategy == FontFallback::AS_SUB && !font_data.found) { + sp_repr_css_set_property(_css_font, "font-family", font_data.getSubstitute().c_str()); + } else { + sp_repr_css_set_property(_css_font, "font-family", font_data.family.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 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::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 nodes in a 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 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) { + 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, 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 *>(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 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 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 with the specified width and height and adds to + * If we're not the top-level SvgBuilder, creates a 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 : -- cgit v1.2.3